From 1748432ddbfc56667aa550c959d8c76a29c38edd Mon Sep 17 00:00:00 2001 From: sevi-kun Date: Sun, 25 May 2025 16:05:57 +0200 Subject: [PATCH] First working version of jellyseerr discord bot --- .env.example | 36 + .gitignore | 43 + README.md | 106 + commands.py | 584 ++++ config.py | 42 + jellyseerr-api.yml | 7528 +++++++++++++++++++++++++++++++++++++++++++ jellyseerr_api.py | 322 ++ main.py | 209 ++ pyproject.toml | 3 + requirements.txt | 7 + run.sh | 21 + utility_commands.py | 348 ++ 12 files changed, 9249 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 commands.py create mode 100644 config.py create mode 100644 jellyseerr-api.yml create mode 100644 jellyseerr_api.py create mode 100644 main.py create mode 100644 pyproject.toml create mode 100644 requirements.txt create mode 100755 run.sh create mode 100644 utility_commands.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ae8534e --- /dev/null +++ b/.env.example @@ -0,0 +1,36 @@ +# Discord Jellyseerr Bot - Environment Variables +# Copy this file to .env and fill in your values + +# Discord Bot Token (required) +DISCORD_BOT_TOKEN=your_discord_bot_token_here + +# Bot command prefix (default is !) +BOT_PREFIX=! + +# Jellyseerr Configuration (required) +JELLYSEERR_URL=http://your-jellyseerr-instance:5055 + +# Jellyseerr Authentication (using local user account) +JELLYSEERR_EMAIL=your_jellyseerr_email@example.com +JELLYSEERR_PASSWORD=your_jellyseerr_password + +# Set to false if you want to disable local login (not recommended) +JELLYSEERR_LOCAL_LOGIN=true + +# Number of days until authentication cookie expires (default: 7) +AUTH_COOKIE_EXPIRY=7 + +# Notifications +# Jellyseerr provides webhooks for notifications, which can be configured +# directly in the Jellyseerr settings. Discord webhooks are recommended +# instead of using this bot for notifications. + +# UI Settings +EMBED_COLOR=0x3498db + +# Debug Settings +# Set to true to enable verbose logging (useful for troubleshooting) +DEBUG_MODE=false + +# API request timeout in seconds +REQUEST_TIMEOUT=30 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d134ee1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +# Python virtual environment +.venv/ +venv/ +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# Environment variables +.env + +# IDE files +.idea/ +.vscode/ +*.swp +*.swo + +# Logs +*.log + +# Bot specific +bot.log +config.json + +# OS specific +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d0506e5 --- /dev/null +++ b/README.md @@ -0,0 +1,106 @@ +# Discord-Jellyseerr Bot + +This Discord bot integrates with Jellyseerr to provide commands for searching, requesting, and getting notifications about media in your Jellyseerr instance. + +## Features + +- Search for movies and TV shows +- Get detailed information about media +- Request movies and TV shows to be added to your library +- Get recommendations for similar content +- Simple interface for interacting with your Jellyseerr instance + +## Installation + +1. Clone this repository: + ``` + git clone https://github.com/yourusername/discord-jellyseerr.git + cd discord-jellyseerr + ``` + +2. Install the required dependencies: + ``` + pip install -r requirements.txt + ``` + +3. Copy the `.env.example` file to `.env` and edit it with your configuration: + ``` + cp .env.example .env + ``` + +4. Edit the `.env` file with your Discord bot token and Jellyseerr API details: + ``` + DISCORD_BOT_TOKEN=your_discord_bot_token_here + JELLYSEERR_URL=http://your-jellyseerr-instance:5055 + JELLYSEERR_EMAIL=your_jellyseerr_email@example.com + JELLYSEERR_PASSWORD=your_jellyseerr_password + ``` + +5. Run the bot: + ``` + python main.py + ``` + + Alternatively, use the provided shell script: + ``` + ./run.sh + ``` + +## Setting up the Discord Bot + +1. Go to the [Discord Developer Portal](https://discord.com/developers/applications) +2. Create a new application and set up a bot +4. Enable the following Privileged Gateway Intents: + - MESSAGE CONTENT INTENT +4. Invite the bot to your server with the following permissions: + - Send Messages + - Embed Links + - Attach Files + - Read Message History + - Use External Emojis + - Add Reactions + +## Jellyseerr Authentication + +This bot uses a local Jellyseerr user account for authentication: + +1. Create or use an existing local user account in Jellyseerr +2. Provide the email address and password in the .env file +3. The bot will automatically authenticate and maintain the session + +Using a dedicated user account provides better tracking of requests and actions performed by the bot. + +**Important**: Jellyseerr requires the email address for login, not the username. + +## Commands + +### Search & Discovery +- `!search ` - Search for movies and TV shows +- `!movie ` - Get detailed information about a movie +- `!tv ` - Get detailed information about a TV show +- `!trending` - Show trending movies and TV shows + +### Requests +- `!request movie ` - Request a movie +- `!request tv [seasons]` - Request a TV show (all or specific seasons) +- `!requests [status] [page]` - List media requests + + + +### Other +- `!help` - Show the help message +- `!status` - Check Jellyseerr server status + +## Webhooks + +For notifications about request approvals, media availability, and other events, Jellyseerr provides built-in webhook support for Discord. Configure these directly in the Jellyseerr settings panel under "Notifications". + +This bot does not handle notifications, as the native webhook integration provides a more reliable solution with customizable notifications. + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. \ No newline at end of file diff --git a/commands.py b/commands.py new file mode 100644 index 0000000..66aaa19 --- /dev/null +++ b/commands.py @@ -0,0 +1,584 @@ +import discord +from discord.ext import commands +import asyncio +import aiohttp +from typing import Optional, List, Dict, Union, Any +import re +import logging + +import config +# We'll import jellyseerr_api later to avoid circular imports + +logger = logging.getLogger('commands') + +# Get the API client from main module (to avoid circular imports) +import sys +jellyseerr_api = None + +def get_api(): + global jellyseerr_api + if jellyseerr_api is None: + # Get the API client instance from the main module + if 'jellyseerr_api' in sys.modules and hasattr(sys.modules['jellyseerr_api'], 'jellyseerr_api'): + jellyseerr_api = sys.modules['jellyseerr_api'].jellyseerr_api + logger.debug("Retrieved jellyseerr_api instance from module") + else: + # This should not happen, but just in case + logger.error("Could not find jellyseerr_api instance") + return jellyseerr_api + +def safe_int_convert(value, default=None): + """Safely convert a value to integer, returning default if conversion fails""" + if value is None: + return default + try: + return int(value) + except (ValueError, TypeError): + return default + +class MediaCommands(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.embed_color = config.EMBED_COLOR + + @commands.command(name="info", aliases=["i"]) + async def get_media_info(self, ctx, media_id = None): + """Get detailed information about a movie or TV show by ID (auto-detects type)""" + if not media_id: + await ctx.send("Please provide a media ID. Example: `!info 550` (for Fight Club)") + return + + media_id = safe_int_convert(media_id) + if not media_id or media_id <= 0: + await ctx.send("Media ID must be a positive number.") + return + + async with ctx.typing(): + try: + api = get_api() + logger.info(f"Looking up media with ID: {media_id}") + + # Try to get as movie first + try: + movie = await api.get_movie_details(media_id) + if movie and movie.get('title'): + # It's a movie, show movie details + embed = discord.Embed( + title=f"{movie.get('title')} ({movie.get('releaseDate', 'Unknown')[:4]})", + description=movie.get('overview'), + color=self.embed_color, + url=f"{config.JELLYSEERR_URL}/movie/{media_id}" + ) + + # Add poster if available + if movie.get('posterPath'): + embed.set_thumbnail(url=f"https://image.tmdb.org/t/p/w500{movie.get('posterPath')}") + + # Add rating and runtime + rating_value = movie.get('voteAverage', 0) + rating_str = f"⭐ {rating_value}/10" if rating_value else "Not rated" + + runtime = movie.get('runtime', 0) + runtime_str = f"{runtime} mins" if runtime else "Unknown" + + embed.add_field(name="Type", value="Movie", inline=True) + embed.add_field(name="Rating", value=rating_str, inline=True) + embed.add_field(name="Runtime", value=runtime_str, inline=True) + + # Add genres + genres = movie.get('genres', []) + if genres: + genres_str = ", ".join([genre.get('name') for genre in genres]) + embed.add_field(name="Genres", value=genres_str, inline=True) + + # Add media status if available + media_info = movie.get('mediaInfo') + if media_info: + status = media_info.get('status') + status_map = { + 1: "Unknown", + 2: "Pending", + 3: "Processing", + 4: "Partially Available", + 5: "Available" + } + status_str = status_map.get(status, "Unknown") + embed.add_field(name="Status", value=status_str, inline=True) + + # Add request button info + if not media_info or media_info.get('status') < 5: + embed.add_field( + name="Request this Movie", + value=f"Use `{config.BOT_PREFIX}request movie {media_id}` to request this movie", + inline=False + ) + + await ctx.send(embed=embed) + return + except Exception as movie_error: + # Not a movie, try as TV show + pass + + # Try as TV show + try: + tv = await api.get_tv_details(media_id) + if tv and tv.get('name'): + # It's a TV show, show TV details + title = tv.get('name') + first_air_date = tv.get('firstAirDate', '') + year = f" ({first_air_date[:4]})" if first_air_date else "" + + embed = discord.Embed( + title=f"{title}{year}", + description=tv.get('overview'), + color=self.embed_color, + url=f"{config.JELLYSEERR_URL}/tv/{media_id}" + ) + + # Add poster if available + if tv.get('posterPath'): + embed.set_thumbnail(url=f"https://image.tmdb.org/t/p/w500{tv.get('posterPath')}") + + # Add rating and seasons info + rating_value = tv.get('voteAverage', 0) + rating_str = f"⭐ {rating_value}/10" if rating_value else "Not rated" + + seasons_count = tv.get('numberOfSeasons', 0) + episodes_count = tv.get('numberOfEpisodes', 0) + seasons_str = f"{seasons_count} seasons ({episodes_count} episodes)" + + embed.add_field(name="Type", value="TV Show", inline=True) + embed.add_field(name="Rating", value=rating_str, inline=True) + embed.add_field(name="Seasons", value=seasons_str, inline=True) + + # Add status + status = tv.get('status', 'Unknown') + embed.add_field(name="Status", value=status, inline=True) + + # Add genres + genres = tv.get('genres', []) + if genres: + genres_str = ", ".join([genre.get('name') for genre in genres]) + embed.add_field(name="Genres", value=genres_str, inline=True) + + # Add networks + networks = tv.get('networks', []) + if networks: + networks_str = ", ".join([network.get('name') for network in networks]) + embed.add_field(name="Networks", value=networks_str, inline=True) + + # Add media status if available + media_info = tv.get('mediaInfo') + if media_info: + status = media_info.get('status') + status_map = { + 1: "Unknown", + 2: "Pending", + 3: "Processing", + 4: "Partially Available", + 5: "Available" + } + status_str = status_map.get(status, "Unknown") + embed.add_field(name="Availability", value=status_str, inline=True) + + # Add seasons list (compact) + seasons = tv.get('seasons', []) + if seasons: + valid_seasons = [ + season for season in seasons + if season.get('seasonNumber', 0) > 0 + ] + + if valid_seasons: + season_nums = [str(season.get('seasonNumber')) for season in valid_seasons] + season_list = ", ".join(season_nums) + embed.add_field( + name=f"Available Seasons", + value=season_list, + inline=False + ) + + # Add request button info + if not media_info or media_info.get('status') < 5: + embed.add_field( + name="Request this Show", + value=f"Use `{config.BOT_PREFIX}request tv {media_id}` to request all seasons or\n" + f"`{config.BOT_PREFIX}request tv {media_id} ` for specific seasons", + inline=False + ) + + await ctx.send(embed=embed) + return + except Exception as tv_error: + # Not a TV show either + pass + + # If we get here, the ID doesn't exist as either movie or TV show + await ctx.send(f"Error: Could not find media with ID {media_id}. This could be because:\n" + f"1. The ID doesn't exist in TMDB\n" + f"2. Your Jellyseerr account doesn't have permission to view this media\n\n" + f"Try using `{config.BOT_PREFIX}testmedia {media_id}` for more information.") + + except Exception as e: + logger.error(f"Error retrieving media details for ID {media_id}: {str(e)}", exc_info=True) + await ctx.send(f"Error retrieving media details: {str(e)}") + + @commands.command(name="search", aliases=["s"]) + async def search_media(self, ctx, *, query: str = None): + """Search for movies and TV shows by title""" + if not query: + await ctx.send("Please provide a search term. Example: `!search Stranger Things`") + return + + if len(query) < 2: + await ctx.send("Search term must be at least 2 characters long.") + return + + async with ctx.typing(): + try: + api = get_api() + results = await api.search(query=query) + + if not results or not results.get('results'): + await ctx.send(f"No results found for '{query}'") + return + + # Filter out person results + media_results = [result for result in results.get('results', []) + if result.get('mediaType') in ['movie', 'tv']] + + if not media_results: + await ctx.send(f"No movie or TV show results found for '{query}'") + return + + # Limit to first 5 results for cleaner output + displayed_results = media_results[:5] + + embed = discord.Embed( + title=f"Search Results for '{query}'", + color=self.embed_color + ) + + for i, result in enumerate(displayed_results, 1): + media_type = result.get('mediaType') + title = result.get('title') if media_type == 'movie' else result.get('name') + year = "" + + if media_type == 'movie' and result.get('releaseDate'): + year = f" ({result.get('releaseDate')[:4]})" + elif media_type == 'tv' and result.get('firstAirDate'): + year = f" ({result.get('firstAirDate')[:4]})" + + media_id = result.get('id') + + embed.add_field( + name=f"{i}. {title}{year} ({media_type.upper()})", + value=f"ID: {media_id}\n{result.get('overview', '')[:100]}...", + inline=False + ) + + if len(media_results) > 5: + embed.set_footer(text=f"Showing 5 out of {len(media_results)} results") + + await ctx.send(embed=embed) + + except Exception as e: + await ctx.send(f"Error searching for media: {str(e)}") + + # Movie and TV commands removed - replaced by the unified info command + + @commands.command(name="trending") + async def get_trending(self, ctx): + """Get trending movies and TV shows""" + async with ctx.typing(): + try: + api = get_api() + trending = await api.discover_trending() + + if not trending or not trending.get('results'): + await ctx.send("No trending media found") + return + + # Get first 5 results + results = trending.get('results', [])[:5] + + embed = discord.Embed( + title=f"Trending Movies & TV Shows", + color=self.embed_color + ) + + for result in results: + media_type = result.get('mediaType') + title = result.get('title') if media_type == 'movie' else result.get('name') + + year = "" + if media_type == 'movie' and result.get('releaseDate'): + year = f" ({result.get('releaseDate')[:4]})" + elif media_type == 'tv' and result.get('firstAirDate'): + year = f" ({result.get('firstAirDate')[:4]})" + + media_id = result.get('id') + overview = result.get('overview', '') + if len(overview) > 100: + overview = f"{overview[:100]}..." + + embed.add_field( + name=f"{title}{year} ({media_type.upper()})", + value=f"ID: {media_id}\n{overview}", + inline=False + ) + + # Add note about the new info command + embed.set_footer(text=f"Tip: Use {config.BOT_PREFIX}info to get details for any result") + + await ctx.send(embed=embed) + + except Exception as e: + await ctx.send(f"Error retrieving trending media: {str(e)}") + +class RequestCommands(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.embed_color = config.EMBED_COLOR + + @commands.command(name="request", aliases=["req"]) + async def create_request(self, ctx, media_type: str = None, media_id = None, *args): + """ + Request a movie or TV show + Examples: + !request movie 123 + !request tv 456 + !request tv 456 1,2,3 (for specific seasons) + + Note: Requests will need to be approved in the Jellyseerr web interface + """ + if not media_type: + await ctx.send("Please specify a media type (`movie` or `tv`). Example: `!request movie 550`") + return + + if media_type.lower() not in ['movie', 'tv']: + await ctx.send("Invalid media type. Use 'movie' or 'tv'.") + return + + media_id = safe_int_convert(media_id) + if not media_id: + await ctx.send(f"Please provide a valid media ID. Example: `!request {media_type} 550`") + return + + if media_id <= 0: + await ctx.send("Media ID must be a positive number.") + return + + async with ctx.typing(): + try: + # Check if there are specific seasons for TV shows + seasons = None + if media_type.lower() == 'tv' and args: + season_arg = args[0] + if season_arg.lower() == 'all': + seasons = 'all' + else: + # Parse comma-separated season numbers + try: + # Check for valid format + if not re.match(r'^[0-9]+(,[0-9]+)*$', season_arg): + await ctx.send("Invalid season format. Use comma-separated numbers (e.g., `1,2,3`) or 'all'.") + return + + seasons = [int(s.strip()) for s in season_arg.split(',')] + + # Check for invalid season numbers + if any(s <= 0 for s in seasons): + await ctx.send("Season numbers must be positive integers.") + return + except ValueError: + await ctx.send("Invalid season format. Use comma-separated numbers or 'all'.") + return + + # Get media details first to show what's being requested + api = get_api() + try: + if media_type.lower() == 'movie': + logger.info(f"Getting movie details for request: {media_id}") + media_details = await api.get_movie_details(media_id) + title = media_details.get('title') + year = media_details.get('releaseDate', '')[:4] if media_details.get('releaseDate') else '' + else: # TV show + logger.info(f"Getting TV details for request: {media_id}") + media_details = await api.get_tv_details(media_id) + title = media_details.get('name') + year = media_details.get('firstAirDate', '')[:4] if media_details.get('firstAirDate') else '' + + if not title: + await ctx.send(f"Could not find {media_type} with ID {media_id}. Please check the ID and try again.") + return + + except Exception as e: + logger.error(f"Error retrieving media details for request: {str(e)}") + await ctx.send(f"Could not retrieve details for {media_type} with ID {media_id}. Please check the ID and try again.") + return + + # Create the request + request_data = { + 'media_type': media_type.lower(), + 'media_id': media_id, + 'is_4k': False + } + + if media_type.lower() == 'tv' and seasons: + request_data['seasons'] = seasons + + logger.info(f"Submitting request for {media_type} ID {media_id}") + + # Submit request + request = await api.create_request(**request_data) + + # Build response embed + if year: + title = f"{title} ({year})" + + request_type = "Movie" if media_type.lower() == "movie" else "TV Show" + + embed = discord.Embed( + title=f"{request_type} Request Submitted", + description=f"**{title}** has been requested!", + color=self.embed_color + ) + + # Add info about which account was used + embed.set_footer(text=f"Requested using Jellyseerr account: {config.JELLYSEERR_EMAIL}") + + status = request.get('status', 0) + status_map = { + 1: "Pending Approval", + 2: "Approved", + 3: "Declined" + } + status_text = status_map.get(status, "Unknown") + + embed.add_field(name="Status", value=status_text, inline=True) + + if media_type.lower() == 'tv' and seasons and seasons != 'all': + embed.add_field(name="Requested Seasons", value=", ".join(str(s) for s in seasons), inline=True) + + # Add poster if available + poster_path = media_details.get('posterPath') + if poster_path: + embed.set_thumbnail(url=f"https://image.tmdb.org/t/p/w500{poster_path}") + + await ctx.send(embed=embed) + + except Exception as e: + logger.error(f"Error creating request: {str(e)}", exc_info=True) + + error_message = str(e) + if "500" in error_message: + await ctx.send(f"Error creating request: Server error (500). This could be because:\n" + f"1. You might not have permission to request this media\n" + f"2. The media ID doesn't exist or is invalid\n" + f"3. The media may already be requested or available\n\n" + f"Try searching for the media first with `{config.BOT_PREFIX}search` to get a valid ID") + elif "401" in error_message or "403" in error_message: + await ctx.send(f"Error creating request: Authentication error. The bot account may not have permission to make requests.") + else: + await ctx.send(f"Error creating request: {str(e)}") + + @commands.command(name="requests", aliases=["reqs"]) + async def list_requests(self, ctx, status: str = "pending", page = 1): + """ + List media requests with optional status filter + Statuses: all, pending, approved, available, processing + """ + valid_statuses = ["all", "pending", "approved", "available", "processing"] + + if status.lower() not in valid_statuses: + await ctx.send(f"Invalid status. Use one of: {', '.join(valid_statuses)}") + return + + page = safe_int_convert(page, 1) + if page <= 0: + await ctx.send("Page number must be a positive integer.") + return + + async with ctx.typing(): + try: + # Get requests with the specified filter + api = get_api() + requests_data = await api.get_requests(filter_status=status.lower(), page=page) + + results = requests_data.get('results', []) + if not results: + await ctx.send(f"No {status} requests found.") + return + + # Build embed response + embed = discord.Embed( + title=f"{status.capitalize()} Media Requests", + color=self.embed_color + ) + + for req in results: + media = req.get('media', {}) + media_type = media.get('mediaType', 'unknown') + + # Get title based on media type + if media_type == 'movie': + title = media.get('title', 'Unknown Movie') + else: + title = media.get('name', 'Unknown TV Show') + + # Get request details + request_id = req.get('id') + status_code = req.get('status', 0) + status_map = { + 1: "📝 Pending", + 2: "✅ Approved", + 3: "❌ Declined", + 4: "⏳ Processing" + } + status_text = status_map.get(status_code, "Unknown") + + # Get requester + requested_by = req.get('requestedBy', {}).get('username', 'Unknown') + + # Format requested date + created_at = req.get('createdAt', '') + request_date = created_at.split('T')[0] if created_at else 'Unknown' + + # Format seasons for TV shows + seasons_text = "" + if media_type == 'tv' and req.get('seasons'): + seasons = req.get('seasons', []) + if seasons: + seasons_str = ", ".join(str(s) for s in seasons) + seasons_text = f"\nSeasons: {seasons_str}" + + embed.add_field( + name=f"{title} ({media_type.upper()})", + value=f"Request ID: {request_id}\n" + f"Status: {status_text}\n" + f"Requested by: {requested_by}\n" + f"Date: {request_date}{seasons_text}", + inline=False + ) + + # Add pagination info if available + page_info = requests_data.get('pageInfo', {}) + if page_info: + total_pages = page_info.get('pages', 1) + total_results = page_info.get('results', 0) + embed.set_footer(text=f"Page {page} of {total_pages} • {total_results} total requests") + + await ctx.send(embed=embed) + + except Exception as e: + await ctx.send(f"Error retrieving requests: {str(e)}") + + # Approve and decline commands have been removed + +# UtilityCommands have been moved to utility_commands.py + +def setup(bot): + bot.add_cog(MediaCommands(bot)) + bot.add_cog(RequestCommands(bot)) + bot.add_cog(UtilityCommands(bot)) \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..0cf618b --- /dev/null +++ b/config.py @@ -0,0 +1,42 @@ +import os +import logging +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() + +# Debug mode +DEBUG_MODE = os.getenv('DEBUG_MODE', 'false').lower() == 'true' +LOG_LEVEL = logging.DEBUG if DEBUG_MODE else logging.INFO + +# Bot configuration +BOT_TOKEN = os.getenv('DISCORD_BOT_TOKEN') +BOT_PREFIX = os.getenv('BOT_PREFIX', '!') # Default prefix is ! if not specified + +# Jellyseerr configuration +JELLYSEERR_URL = os.getenv('JELLYSEERR_URL') +JELLYSEERR_EMAIL = os.getenv('JELLYSEERR_EMAIL') +JELLYSEERR_PASSWORD = os.getenv('JELLYSEERR_PASSWORD') +JELLYSEERR_LOCAL_LOGIN = os.getenv('JELLYSEERR_LOCAL_LOGIN', 'true').lower() == 'true' + +# Bot settings +EMBED_COLOR = int(os.getenv('EMBED_COLOR', '0x3498db'), 0) # Default color is blue + +# Authentication settings +AUTH_COOKIE_EXPIRY = int(os.getenv('AUTH_COOKIE_EXPIRY', '7')) # Days until cookie expires + +# Notification settings are not used as Jellyseerr webhooks are preferred +# You can configure webhooks directly in Jellyseerr's settings + +# Debug settings +REQUEST_TIMEOUT = int(os.getenv('REQUEST_TIMEOUT', '30')) # Seconds to wait for API response + +# Validation +if not BOT_TOKEN: + raise ValueError("No Discord bot token found. Please set DISCORD_BOT_TOKEN in your .env file.") + +if not JELLYSEERR_URL: + raise ValueError("No Jellyseerr URL found. Please set JELLYSEERR_URL in your .env file.") + +if JELLYSEERR_LOCAL_LOGIN and (not JELLYSEERR_EMAIL or not JELLYSEERR_PASSWORD): + raise ValueError("Jellyseerr email and password are required for local login. Please set JELLYSEERR_EMAIL and JELLYSEERR_PASSWORD in your .env file.") \ No newline at end of file diff --git a/jellyseerr-api.yml b/jellyseerr-api.yml new file mode 100644 index 0000000..8437056 --- /dev/null +++ b/jellyseerr-api.yml @@ -0,0 +1,7528 @@ +openapi: '3.0.2' +info: + title: 'Jellyseerr API' + version: '1.0.0' + description: | + This is the documentation for the Jellyseerr API backend. + + Two primary authentication methods are supported: + + - **Cookie Authentication**: A valid sign-in to the `/auth/plex` or `/auth/local` will generate a valid authentication cookie. + - **API Key Authentication**: Sign-in is also possible by passing an `X-Api-Key` header along with a valid API Key generated by Jellyseerr. +tags: + - name: public + description: Public API endpoints requiring no authentication. + - name: settings + description: Endpoints related to Jellyseerr's settings and configuration. + - name: auth + description: Endpoints related to logging in or out, and the currently authenticated user. + - name: users + description: Endpoints related to user management. + - name: search + description: Endpoints related to search and discovery. + - name: request + description: Endpoints related to request management. + - name: movies + description: Endpoints related to retrieving movies and their details. + - name: tv + description: Endpoints related to retrieving TV series and their details. + - name: other + description: Endpoints related to other TMDB data + - name: person + description: Endpoints related to retrieving person details. + - name: media + description: Endpoints related to media management. + - name: collection + description: Endpoints related to retrieving collection details. + - name: service + description: Endpoints related to getting service (Radarr/Sonarr) details. + - name: watchlist + description: Collection of media to watch later + - name: blacklist + description: Blacklisted media from discovery page. +servers: + - url: '{server}/api/v1' + variables: + server: + default: http://localhost:5055 + +components: + schemas: + Blacklist: + type: object + properties: + tmdbId: + type: number + example: 1 + title: + type: string + media: + $ref: '#/components/schemas/MediaInfo' + userId: + type: number + example: 1 + Watchlist: + type: object + properties: + id: + type: integer + example: 1 + readOnly: true + tmdbId: + type: number + example: 1 + ratingKey: + type: string + type: + type: string + title: + type: string + media: + $ref: '#/components/schemas/MediaInfo' + createdAt: + type: string + example: '2020-09-12T10:00:27.000Z' + readOnly: true + updatedAt: + type: string + example: '2020-09-12T10:00:27.000Z' + readOnly: true + requestedBy: + $ref: '#/components/schemas/User' + User: + type: object + properties: + id: + type: integer + example: 1 + readOnly: true + email: + type: string + example: 'hey@itsme.com' + readOnly: true + username: + type: string + plexUsername: + type: string + readOnly: true + plexToken: + type: string + readOnly: true + jellyfinAuthToken: + type: string + readOnly: true + userType: + type: integer + example: 1 + readOnly: true + permissions: + type: number + example: 0 + avatar: + type: string + readOnly: true + createdAt: + type: string + example: '2020-09-02T05:02:23.000Z' + readOnly: true + updatedAt: + type: string + example: '2020-09-02T05:02:23.000Z' + readOnly: true + requestCount: + type: number + example: 5 + readOnly: true + required: + - id + - email + - createdAt + - updatedAt + UserSettings: + type: object + properties: + locale: + type: string + discoverRegion: + type: string + originalLanguage: + type: string + streamingRegion: + type: string + MainSettings: + type: object + properties: + apiKey: + type: string + readOnly: true + appLanguage: + type: string + example: en + applicationTitle: + type: string + example: Jellyseerr + applicationUrl: + type: string + example: https://os.example.com + hideAvailable: + type: boolean + example: false + partialRequestsEnabled: + type: boolean + example: false + localLogin: + type: boolean + example: true + mediaServerType: + type: number + example: 1 + newPlexLogin: + type: boolean + example: true + defaultPermissions: + type: number + example: 32 + enableSpecialEpisodes: + type: boolean + example: false + NetworkSettings: + type: object + properties: + csrfProtection: + type: boolean + example: false + trustProxy: + type: boolean + example: true + PlexLibrary: + type: object + properties: + id: + type: string + name: + type: string + example: Movies + enabled: + type: boolean + example: false + required: + - id + - name + - enabled + PlexSettings: + type: object + properties: + name: + type: string + example: 'Main Server' + readOnly: true + machineId: + type: string + example: '1234123412341234' + readOnly: true + ip: + type: string + example: '127.0.0.1' + port: + type: number + example: 32400 + useSsl: + type: boolean + nullable: true + libraries: + type: array + readOnly: true + items: + $ref: '#/components/schemas/PlexLibrary' + webAppUrl: + type: string + nullable: true + example: 'https://app.plex.tv/desktop' + required: + - name + - machineId + - ip + - port + PlexConnection: + type: object + properties: + protocol: + type: string + example: 'https' + address: + type: string + example: '127.0.0.1' + port: + type: number + example: 32400 + uri: + type: string + example: 'https://127-0-0-1.2ab6ce1a093d465e910def96cf4e4799.plex.direct:32400' + local: + type: boolean + example: true + status: + type: number + example: 200 + message: + type: string + example: 'OK' + required: + - protocol + - address + - port + - uri + - local + PlexDevice: + type: object + properties: + name: + type: string + example: 'My Plex Server' + product: + type: string + example: 'Plex Media Server' + productVersion: + type: string + example: '1.21' + platform: + type: string + example: 'Linux' + platformVersion: + type: string + example: 'default/linux/amd64/17.1/systemd' + device: + type: string + example: 'PC' + clientIdentifier: + type: string + example: '85a943ce-a0cc-4d2a-a4ec-f74f06e40feb' + createdAt: + type: string + example: '2021-01-01T00:00:00.000Z' + lastSeenAt: + type: string + example: '2021-01-01T00:00:00.000Z' + provides: + type: array + items: + type: string + example: 'server' + owned: + type: boolean + example: true + ownerID: + type: string + example: '12345' + home: + type: boolean + example: true + sourceTitle: + type: string + example: 'xyzabc' + accessToken: + type: string + example: 'supersecretaccesstoken' + publicAddress: + type: string + example: '127.0.0.1' + httpsRequired: + type: boolean + example: true + synced: + type: boolean + example: true + relay: + type: boolean + example: true + dnsRebindingProtection: + type: boolean + example: false + natLoopbackSupported: + type: boolean + example: false + publicAddressMatches: + type: boolean + example: false + presence: + type: boolean + example: true + connection: + type: array + items: + $ref: '#/components/schemas/PlexConnection' + required: + - name + - product + - productVersion + - platform + - device + - clientIdentifier + - createdAt + - lastSeenAt + - provides + - owned + - connection + JellyfinLibrary: + type: object + properties: + id: + type: string + name: + type: string + example: Movies + enabled: + type: boolean + example: false + required: + - id + - name + - enabled + JellyfinSettings: + type: object + properties: + name: + type: string + example: 'Main Server' + readOnly: true + hostname: + type: string + example: 'http://my.jellyfin.host' + externalHostname: + type: string + example: 'http://my.jellyfin.host' + jellyfinForgotPasswordUrl: + type: string + example: 'http://my.jellyfin.host/web/index.html#!/forgotpassword.html' + adminUser: + type: string + example: 'admin' + adminPass: + type: string + example: 'mypassword' + libraries: + type: array + readOnly: true + items: + $ref: '#/components/schemas/JellyfinLibrary' + serverID: + type: string + readOnly: true + TautulliSettings: + type: object + properties: + hostname: + type: string + nullable: true + example: 'tautulli.example.com' + port: + type: number + nullable: true + example: 8181 + useSsl: + type: boolean + nullable: true + apiKey: + type: string + nullable: true + externalUrl: + type: string + nullable: true + RadarrSettings: + type: object + properties: + id: + type: number + example: 0 + readOnly: true + name: + type: string + example: 'Radarr Main' + hostname: + type: string + example: '127.0.0.1' + port: + type: number + example: 7878 + apiKey: + type: string + example: 'exampleapikey' + useSsl: + type: boolean + example: false + baseUrl: + type: string + activeProfileId: + type: number + example: 1 + activeProfileName: + type: string + example: 720p/1080p + activeDirectory: + type: string + example: '/movies' + is4k: + type: boolean + example: false + minimumAvailability: + type: string + example: 'In Cinema' + isDefault: + type: boolean + example: false + externalUrl: + type: string + example: http://radarr.example.com + syncEnabled: + type: boolean + example: false + preventSearch: + type: boolean + example: false + required: + - name + - hostname + - port + - apiKey + - useSsl + - activeProfileId + - activeProfileName + - activeDirectory + - is4k + - minimumAvailability + - isDefault + SonarrSettings: + type: object + properties: + id: + type: number + example: 0 + readOnly: true + name: + type: string + example: 'Sonarr Main' + hostname: + type: string + example: '127.0.0.1' + port: + type: number + example: 8989 + apiKey: + type: string + example: 'exampleapikey' + useSsl: + type: boolean + example: false + baseUrl: + type: string + activeProfileId: + type: number + example: 1 + activeProfileName: + type: string + example: 720p/1080p + activeDirectory: + type: string + example: '/tv/' + activeLanguageProfileId: + type: number + example: 1 + activeAnimeProfileId: + type: number + nullable: true + activeAnimeLanguageProfileId: + type: number + nullable: true + activeAnimeProfileName: + type: string + example: 720p/1080p + nullable: true + activeAnimeDirectory: + type: string + nullable: true + is4k: + type: boolean + example: false + enableSeasonFolders: + type: boolean + example: false + isDefault: + type: boolean + example: false + externalUrl: + type: string + example: http://radarr.example.com + syncEnabled: + type: boolean + example: false + preventSearch: + type: boolean + example: false + required: + - name + - hostname + - port + - apiKey + - useSsl + - activeProfileId + - activeProfileName + - activeDirectory + - is4k + - enableSeasonFolders + - isDefault + ServarrTag: + type: object + properties: + id: + type: number + example: 1 + label: + type: string + example: A Label + PublicSettings: + type: object + properties: + initialized: + type: boolean + example: false + MovieResult: + type: object + required: + - id + - mediaType + - title + properties: + id: + type: number + example: 1234 + mediaType: + type: string + popularity: + type: number + example: 10 + posterPath: + type: string + backdropPath: + type: string + voteCount: + type: number + voteAverage: + type: number + genreIds: + type: array + items: + type: number + overview: + type: string + example: 'Overview of the movie' + originalLanguage: + type: string + example: 'en' + title: + type: string + example: Movie Title + originalTitle: + type: string + example: Original Movie Title + releaseDate: + type: string + adult: + type: boolean + example: false + video: + type: boolean + example: false + mediaInfo: + $ref: '#/components/schemas/MediaInfo' + TvResult: + type: object + properties: + id: + type: number + example: 1234 + mediaType: + type: string + popularity: + type: number + example: 10 + posterPath: + type: string + backdropPath: + type: string + voteCount: + type: number + voteAverage: + type: number + genreIds: + type: array + items: + type: number + overview: + type: string + example: 'Overview of the movie' + originalLanguage: + type: string + example: 'en' + name: + type: string + example: TV Show Name + originalName: + type: string + example: Original TV Show Name + originCountry: + type: array + items: + type: string + firstAirDate: + type: string + mediaInfo: + $ref: '#/components/schemas/MediaInfo' + PersonResult: + type: object + properties: + id: + type: number + example: 12345 + profilePath: + type: string + adult: + type: boolean + example: false + mediaType: + type: string + default: 'person' + knownFor: + type: array + items: + oneOf: + - $ref: '#/components/schemas/MovieResult' + - $ref: '#/components/schemas/TvResult' + Genre: + type: object + properties: + id: + type: number + example: 1 + name: + type: string + example: Adventure + Company: + type: object + properties: + id: + type: number + example: 1 + logo_path: + type: string + nullable: true + name: + type: string + ProductionCompany: + type: object + properties: + id: + type: number + example: 1 + logoPath: + type: string + nullable: true + originCountry: + type: string + name: + type: string + Network: + type: object + properties: + id: + type: number + example: 1 + logoPath: + type: string + nullable: true + originCountry: + type: string + name: + type: string + RelatedVideo: + type: object + properties: + url: + type: string + example: https://www.youtube.com/watch?v=9qhL2_UxXM0/ + key: + type: string + example: 9qhL2_UxXM0 + name: + type: string + example: Trailer for some movie (1978) + size: + type: number + example: 1080 + type: + type: string + example: Trailer + enum: + - Clip + - Teaser + - Trailer + - Featurette + - Opening Credits + - Behind the Scenes + - Bloopers + site: + type: string + enum: + - 'YouTube' + MovieDetails: + type: object + properties: + id: + type: number + example: 123 + readOnly: true + imdbId: + type: string + example: 'tt123' + adult: + type: boolean + backdropPath: + type: string + posterPath: + type: string + budget: + type: number + example: 1000000 + genres: + type: array + items: + $ref: '#/components/schemas/Genre' + homepage: + type: string + relatedVideos: + type: array + items: + $ref: '#/components/schemas/RelatedVideo' + originalLanguage: + type: string + originalTitle: + type: string + overview: + type: string + popularity: + type: number + productionCompanies: + type: array + items: + $ref: '#/components/schemas/ProductionCompany' + productionCountries: + type: array + items: + type: object + properties: + iso_3166_1: + type: string + name: + type: string + releaseDate: + type: string + releases: + type: object + properties: + results: + type: array + items: + type: object + properties: + iso_3166_1: + type: string + example: 'US' + rating: + type: string + nullable: true + release_dates: + type: array + items: + type: object + properties: + certification: + type: string + example: 'PG-13' + iso_639_1: + type: string + nullable: true + note: + type: string + nullable: true + example: 'Blu ray' + release_date: + type: string + example: '2017-07-12T00:00:00.000Z' + type: + type: number + example: 1 + revenue: + type: number + nullable: true + runtime: + type: number + spokenLanguages: + type: array + items: + $ref: '#/components/schemas/SpokenLanguage' + status: + type: string + tagline: + type: string + title: + type: string + video: + type: boolean + voteAverage: + type: number + voteCount: + type: number + credits: + type: object + properties: + cast: + type: array + items: + $ref: '#/components/schemas/Cast' + crew: + type: array + items: + $ref: '#/components/schemas/Crew' + collection: + type: object + properties: + id: + type: number + example: 1 + name: + type: string + example: A collection + posterPath: + type: string + backdropPath: + type: string + externalIds: + $ref: '#/components/schemas/ExternalIds' + mediaInfo: + $ref: '#/components/schemas/MediaInfo' + watchProviders: + type: array + items: + $ref: '#/components/schemas/WatchProviders' + Episode: + type: object + properties: + id: + type: number + name: + type: string + airDate: + type: string + nullable: true + episodeNumber: + type: number + overview: + type: string + productionCode: + type: string + seasonNumber: + type: number + showId: + type: number + stillPath: + type: string + nullable: true + voteAverage: + type: number + voteCount: + type: number + Season: + type: object + properties: + id: + type: number + airDate: + type: string + nullable: true + episodeCount: + type: number + name: + type: string + overview: + type: string + posterPath: + type: string + seasonNumber: + type: number + episodes: + type: array + items: + $ref: '#/components/schemas/Episode' + TvDetails: + type: object + properties: + id: + type: number + example: 123 + backdropPath: + type: string + posterPath: + type: string + contentRatings: + type: object + properties: + results: + type: array + items: + type: object + properties: + iso_3166_1: + type: string + example: 'US' + rating: + type: string + example: 'TV-14' + createdBy: + type: array + items: + type: object + properties: + id: + type: number + name: + type: string + gender: + type: number + profilePath: + type: string + nullable: true + episodeRunTime: + type: array + items: + type: number + firstAirDate: + type: string + genres: + type: array + items: + $ref: '#/components/schemas/Genre' + homepage: + type: string + inProduction: + type: boolean + languages: + type: array + items: + type: string + lastAirDate: + type: string + lastEpisodeToAir: + $ref: '#/components/schemas/Episode' + name: + type: string + nextEpisodeToAir: + $ref: '#/components/schemas/Episode' + networks: + type: array + items: + $ref: '#/components/schemas/ProductionCompany' + numberOfEpisodes: + type: number + numberOfSeason: + type: number + originCountry: + type: array + items: + type: string + originalLanguage: + type: string + originalName: + type: string + overview: + type: string + popularity: + type: number + productionCompanies: + type: array + items: + $ref: '#/components/schemas/ProductionCompany' + productionCountries: + type: array + items: + type: object + properties: + iso_3166_1: + type: string + name: + type: string + spokenLanguages: + type: array + items: + $ref: '#/components/schemas/SpokenLanguage' + seasons: + type: array + items: + $ref: '#/components/schemas/Season' + status: + type: string + tagline: + type: string + type: + type: string + voteAverage: + type: number + voteCount: + type: number + credits: + type: object + properties: + cast: + type: array + items: + $ref: '#/components/schemas/Cast' + crew: + type: array + items: + $ref: '#/components/schemas/Crew' + externalIds: + $ref: '#/components/schemas/ExternalIds' + keywords: + type: array + items: + $ref: '#/components/schemas/Keyword' + mediaInfo: + $ref: '#/components/schemas/MediaInfo' + watchProviders: + type: array + items: + $ref: '#/components/schemas/WatchProviders' + MediaRequest: + type: object + properties: + id: + type: number + example: 123 + readOnly: true + status: + type: number + example: 0 + description: Status of the request. 1 = PENDING APPROVAL, 2 = APPROVED, 3 = DECLINED + readOnly: true + media: + $ref: '#/components/schemas/MediaInfo' + createdAt: + type: string + example: '2020-09-12T10:00:27.000Z' + readOnly: true + updatedAt: + type: string + example: '2020-09-12T10:00:27.000Z' + readOnly: true + requestedBy: + $ref: '#/components/schemas/User' + modifiedBy: + anyOf: + - $ref: '#/components/schemas/User' + - type: string + nullable: true + is4k: + type: boolean + example: false + serverId: + type: number + profileId: + type: number + rootFolder: + type: string + required: + - id + - status + MediaInfo: + type: object + properties: + id: + type: number + readOnly: true + tmdbId: + type: number + readOnly: true + tvdbId: + type: number + readOnly: true + nullable: true + status: + type: number + example: 0 + description: Availability of the media. 1 = `UNKNOWN`, 2 = `PENDING`, 3 = `PROCESSING`, 4 = `PARTIALLY_AVAILABLE`, 5 = `AVAILABLE`, 6 = `DELETED` + requests: + type: array + readOnly: true + items: + $ref: '#/components/schemas/MediaRequest' + createdAt: + type: string + example: '2020-09-12T10:00:27.000Z' + readOnly: true + updatedAt: + type: string + example: '2020-09-12T10:00:27.000Z' + readOnly: true + Cast: + type: object + properties: + id: + type: number + example: 123 + castId: + type: number + example: 1 + character: + type: string + example: Some Character Name + creditId: + type: string + gender: + type: number + name: + type: string + example: Some Persons Name + order: + type: number + profilePath: + type: string + nullable: true + Crew: + type: object + properties: + id: + type: number + example: 123 + creditId: + type: string + gender: + type: number + name: + type: string + example: Some Persons Name + job: + type: string + department: + type: string + profilePath: + type: string + nullable: true + ExternalIds: + type: object + properties: + facebookId: + type: string + nullable: true + freebaseId: + type: string + nullable: true + freebaseMid: + type: string + nullable: true + imdbId: + type: string + nullable: true + instagramId: + type: string + nullable: true + tvdbId: + type: number + nullable: true + tvrageId: + type: number + nullable: true + twitterId: + type: string + nullable: true + ServiceProfile: + type: object + properties: + id: + type: number + example: 1 + name: + type: string + example: 720p/1080p + PageInfo: + type: object + properties: + page: + type: number + example: 1 + pages: + type: number + example: 10 + results: + type: number + example: 100 + DiscordSettings: + type: object + properties: + enabled: + type: boolean + example: false + types: + type: number + example: 2 + options: + type: object + properties: + botUsername: + type: string + botAvatarUrl: + type: string + webhookUrl: + type: string + webhookRoleId: + type: string + enableMentions: + type: boolean + SlackSettings: + type: object + properties: + enabled: + type: boolean + example: false + types: + type: number + example: 2 + options: + type: object + properties: + webhookUrl: + type: string + WebPushSettings: + type: object + properties: + enabled: + type: boolean + example: false + types: + type: number + example: 2 + WebhookSettings: + type: object + properties: + enabled: + type: boolean + example: false + types: + type: number + example: 2 + options: + type: object + properties: + webhookUrl: + type: string + authHeader: + type: string + jsonPayload: + type: string + TelegramSettings: + type: object + properties: + enabled: + type: boolean + example: false + types: + type: number + example: 2 + options: + type: object + properties: + botUsername: + type: string + botAPI: + type: string + chatId: + type: string + messageThreadId: + type: string + sendSilently: + type: boolean + PushbulletSettings: + type: object + properties: + enabled: + type: boolean + example: false + types: + type: number + example: 2 + options: + type: object + properties: + accessToken: + type: string + channelTag: + type: string + nullable: true + PushoverSettings: + type: object + properties: + enabled: + type: boolean + example: false + types: + type: number + example: 2 + options: + type: object + properties: + accessToken: + type: string + userToken: + type: string + sound: + type: string + GotifySettings: + type: object + properties: + enabled: + type: boolean + example: false + types: + type: number + example: 2 + options: + type: object + properties: + url: + type: string + token: + type: string + NtfySettings: + type: object + properties: + enabled: + type: boolean + example: false + types: + type: number + example: 2 + options: + type: object + properties: + url: + type: string + topic: + type: string + authMethodUsernamePassword: + type: boolean + username: + type: string + password: + type: string + authMethodToken: + type: boolean + token: + type: string + LunaSeaSettings: + type: object + properties: + enabled: + type: boolean + example: false + types: + type: number + example: 2 + options: + type: object + properties: + webhookUrl: + type: string + profileName: + type: string + NotificationEmailSettings: + type: object + properties: + enabled: + type: boolean + example: false + types: + type: number + example: 2 + options: + type: object + properties: + emailFrom: + type: string + example: no-reply@example.com + senderName: + type: string + example: Jellyseerr + smtpHost: + type: string + example: 127.0.0.1 + smtpPort: + type: number + example: 465 + secure: + type: boolean + example: false + ignoreTls: + type: boolean + example: false + requireTls: + type: boolean + example: false + authUser: + type: string + nullable: true + authPass: + type: string + nullable: true + allowSelfSigned: + type: boolean + example: false + Job: + type: object + properties: + id: + type: string + example: job-name + type: + type: string + enum: [process, command] + interval: + type: string + enum: [short, long, fixed] + name: + type: string + example: A Job Name + nextExecutionTime: + type: string + example: '2020-09-02T05:02:23.000Z' + running: + type: boolean + example: false + PersonDetails: + type: object + properties: + id: + type: number + example: 1 + name: + type: string + deathday: + type: string + knownForDepartment: + type: string + alsoKnownAs: + type: array + items: + type: string + gender: + type: string + biography: + type: string + popularity: + type: string + placeOfBirth: + type: string + profilePath: + type: string + adult: + type: boolean + imdbId: + type: string + homepage: + type: string + CreditCast: + type: object + properties: + id: + type: number + example: 1 + originalLanguage: + type: string + episodeCount: + type: number + overview: + type: string + originCountry: + type: array + items: + type: string + originalName: + type: string + voteCount: + type: number + name: + type: string + mediaType: + type: string + popularity: + type: number + creditId: + type: string + backdropPath: + type: string + firstAirDate: + type: string + voteAverage: + type: number + genreIds: + type: array + items: + type: number + posterPath: + type: string + originalTitle: + type: string + video: + type: boolean + title: + type: string + adult: + type: boolean + releaseDate: + type: string + character: + type: string + mediaInfo: + $ref: '#/components/schemas/MediaInfo' + CreditCrew: + type: object + properties: + id: + type: number + example: 1 + originalLanguage: + type: string + episodeCount: + type: number + overview: + type: string + originCountry: + type: array + items: + type: string + originalName: + type: string + voteCount: + type: number + name: + type: string + mediaType: + type: string + popularity: + type: number + creditId: + type: string + backdropPath: + type: string + firstAirDate: + type: string + voteAverage: + type: number + genreIds: + type: array + items: + type: number + posterPath: + type: string + originalTitle: + type: string + video: + type: boolean + title: + type: string + adult: + type: boolean + releaseDate: + type: string + department: + type: string + job: + type: string + mediaInfo: + $ref: '#/components/schemas/MediaInfo' + Keyword: + type: object + properties: + id: + type: number + example: 1 + name: + type: string + example: 'anime' + SpokenLanguage: + type: object + properties: + englishName: + type: string + example: 'English' + nullable: true + iso_639_1: + type: string + example: 'en' + name: + type: string + example: 'English' + Collection: + type: object + properties: + id: + type: number + example: 123 + name: + type: string + example: A Movie Collection + overview: + type: string + example: Overview of collection + posterPath: + type: string + backdropPath: + type: string + parts: + type: array + items: + $ref: '#/components/schemas/MovieResult' + SonarrSeries: + type: object + properties: + title: + type: string + example: COVID-25 + sortTitle: + type: string + example: covid 25 + seasonCount: + type: number + example: 1 + status: + type: string + example: upcoming + overview: + type: string + example: The thread is picked up again by Marianne Schmidt which ... + network: + type: string + example: CBS + airTime: + type: string + example: 02:15 + images: + type: array + items: + type: object + properties: + coverType: + type: string + example: banner + url: + type: string + example: /sonarr/MediaCoverProxy/6467f05d9872726ad08cbf920e5fee4bf69198682260acab8eab5d3c2c958e92/5c8f116c6aa5c.jpg + remotePoster: + type: string + example: https://artworks.thetvdb.com/banners/posters/5c8f116129983.jpg + seasons: + type: array + items: + type: object + properties: + seasonNumber: + type: number + example: 1 + monitored: + type: boolean + example: true + year: + type: number + example: 2015 + path: + type: string + profileId: + type: number + languageProfileId: + type: number + seasonFolder: + type: boolean + monitored: + type: boolean + useSceneNumbering: + type: boolean + runtime: + type: number + tvdbId: + type: number + example: 12345 + tvRageId: + type: number + tvMazeId: + type: number + firstAired: + type: string + lastInfoSync: + type: string + nullable: true + seriesType: + type: string + cleanTitle: + type: string + imdbId: + type: string + titleSlug: + type: string + certification: + type: string + genres: + type: array + items: + type: string + tags: + type: array + items: + type: string + added: + type: string + ratings: + type: array + items: + type: object + properties: + votes: + type: number + value: + type: number + qualityProfileId: + type: number + id: + type: number + nullable: true + rootFolderPath: + type: string + nullable: true + addOptions: + type: array + items: + type: object + properties: + ignoreEpisodesWithFiles: + type: boolean + nullable: true + ignoreEpisodesWithoutFiles: + type: boolean + nullable: true + searchForMissingEpisodes: + type: boolean + nullable: true + UserSettingsNotifications: + type: object + properties: + notificationTypes: + $ref: '#/components/schemas/NotificationAgentTypes' + emailEnabled: + type: boolean + pgpKey: + type: string + nullable: true + discordEnabled: + type: boolean + discordEnabledTypes: + type: number + nullable: true + discordId: + type: string + nullable: true + pushbulletAccessToken: + type: string + nullable: true + pushoverApplicationToken: + type: string + nullable: true + pushoverUserKey: + type: string + nullable: true + pushoverSound: + type: string + nullable: true + telegramEnabled: + type: boolean + telegramBotUsername: + type: string + nullable: true + telegramChatId: + type: string + nullable: true + telegramMessageThreadId: + type: string + nullable: true + telegramSendSilently: + type: boolean + nullable: true + NotificationAgentTypes: + type: object + properties: + discord: + type: number + email: + type: number + pushbullet: + type: number + pushover: + type: number + slack: + type: number + telegram: + type: number + webhook: + type: number + webpush: + type: number + WatchProviders: + type: array + items: + type: object + properties: + iso_3166_1: + type: string + link: + type: string + buy: + type: array + items: + $ref: '#/components/schemas/WatchProviderDetails' + flatrate: + items: + $ref: '#/components/schemas/WatchProviderDetails' + WatchProviderDetails: + type: object + properties: + displayPriority: + type: number + logoPath: + type: string + id: + type: number + name: + type: string + Issue: + type: object + properties: + id: + type: number + example: 1 + issueType: + type: number + example: 1 + media: + $ref: '#/components/schemas/MediaInfo' + createdBy: + $ref: '#/components/schemas/User' + modifiedBy: + $ref: '#/components/schemas/User' + comments: + type: array + items: + $ref: '#/components/schemas/IssueComment' + IssueComment: + type: object + properties: + id: + type: number + example: 1 + user: + $ref: '#/components/schemas/User' + message: + type: string + example: A comment + DiscoverSlider: + type: object + properties: + id: + type: number + example: 1 + type: + type: number + example: 1 + title: + type: string + nullable: true + isBuiltIn: + type: boolean + enabled: + type: boolean + data: + type: string + example: '1234' + nullable: true + required: + - type + - enabled + - title + - data + WatchProviderRegion: + type: object + properties: + iso_3166_1: + type: string + english_name: + type: string + native_name: + type: string + OverrideRule: + type: object + properties: + id: + type: string + Certification: + type: object + properties: + certification: + type: string + example: 'PG-13' + meaning: + type: string + example: 'Some material may be inappropriate for children under 13.' + nullable: true + order: + type: number + example: 3 + nullable: true + required: + - certification + + CertificationResponse: + type: object + properties: + certifications: + type: object + additionalProperties: + type: array + items: + $ref: '#/components/schemas/Certification' + example: + certifications: + US: + - certification: 'G' + meaning: 'All ages admitted' + order: 1 + - certification: 'PG' + meaning: 'Some material may not be suitable for children under 10.' + order: 2 + securitySchemes: + cookieAuth: + type: apiKey + name: connect.sid + in: cookie + apiKey: + type: apiKey + in: header + name: X-Api-Key + +paths: + /status: + get: + summary: Get Jellyseerr status + description: Returns the current Jellyseerr status in a JSON object. + security: [] + tags: + - public + responses: + '200': + description: Returned status + content: + application/json: + schema: + type: object + properties: + version: + type: string + example: 1.0.0 + commitTag: + type: string + updateAvailable: + type: boolean + commitsBehind: + type: number + restartRequired: + type: boolean + /status/appdata: + get: + summary: Get application data volume status + description: For Docker installs, returns whether or not the volume mount was configured properly. Always returns true for non-Docker installs. + security: [] + tags: + - public + responses: + '200': + description: Application data volume status and path + content: + application/json: + schema: + type: object + properties: + appData: + type: boolean + example: true + appDataPath: + type: string + example: /app/config + appDataPermissions: + type: boolean + example: true + /settings/main: + get: + summary: Get main settings + description: Retrieves all main settings in a JSON object. + tags: + - settings + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/MainSettings' + post: + summary: Update main settings + description: Updates main settings with the provided values. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/MainSettings' + responses: + '200': + description: 'Values were sucessfully updated' + content: + application/json: + schema: + $ref: '#/components/schemas/MainSettings' + /settings/network: + get: + summary: Get network settings + description: Retrieves all network settings in a JSON object. + tags: + - settings + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/MainSettings' + post: + summary: Update network settings + description: Updates network settings with the provided values. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/NetworkSettings' + responses: + '200': + description: 'Values were sucessfully updated' + content: + application/json: + schema: + $ref: '#/components/schemas/NetworkSettings' + /settings/main/regenerate: + post: + summary: Get main settings with newly-generated API key + description: Returns main settings in a JSON object, using the new API key. + tags: + - settings + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/MainSettings' + /settings/jellyfin: + get: + summary: Get Jellyfin settings + description: Retrieves current Jellyfin settings. + tags: + - settings + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/JellyfinSettings' + post: + summary: Update Jellyfin settings + description: Updates Jellyfin settings with the provided values. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/JellyfinSettings' + responses: + '200': + description: 'Values were successfully updated' + content: + application/json: + schema: + $ref: '#/components/schemas/JellyfinSettings' + /settings/jellyfin/library: + get: + summary: Get Jellyfin libraries + description: Returns a list of Jellyfin libraries in a JSON array. + tags: + - settings + parameters: + - in: query + name: sync + description: Syncs the current libraries with the current Jellyfin server + schema: + type: string + nullable: true + - in: query + name: enable + explode: false + allowReserved: true + description: Comma separated list of libraries to enable. Any libraries not passed will be disabled! + schema: + type: string + nullable: true + responses: + '200': + description: 'Jellyfin libraries returned' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/JellyfinLibrary' + /settings/jellyfin/users: + get: + summary: Get Jellyfin Users + description: Returns a list of Jellyfin Users in a JSON array. + tags: + - settings + - users + responses: + '200': + description: Jellyfin users returned + content: + application/json: + schema: + type: array + items: + type: object + properties: + username: + type: string + userId: + type: integer + /settings/jellyfin/sync: + get: + summary: Get status of full Jellyfin library sync + description: Returns sync progress in a JSON array. + tags: + - settings + responses: + '200': + description: Status of Jellyfin sync + content: + application/json: + schema: + type: object + properties: + running: + type: boolean + example: false + progress: + type: number + example: 0 + total: + type: number + example: 100 + currentLibrary: + $ref: '#/components/schemas/JellyfinLibrary' + libraries: + type: array + items: + $ref: '#/components/schemas/JellyfinLibrary' + post: + summary: Start full Jellyfin library sync + description: Runs a full Jellyfin library sync and returns the progress in a JSON array. + tags: + - settings + requestBody: + content: + application/json: + schema: + type: object + properties: + cancel: + type: boolean + example: false + start: + type: boolean + example: false + responses: + '200': + description: Status of Jellyfin sync + content: + application/json: + schema: + type: object + properties: + running: + type: boolean + example: false + progress: + type: number + example: 0 + total: + type: number + example: 100 + currentLibrary: + $ref: '#/components/schemas/JellyfinLibrary' + libraries: + type: array + items: + $ref: '#/components/schemas/JellyfinLibrary' + /settings/plex: + get: + summary: Get Plex settings + description: Retrieves current Plex settings. + tags: + - settings + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/PlexSettings' + post: + summary: Update Plex settings + description: Updates Plex settings with the provided values. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PlexSettings' + responses: + '200': + description: 'Values were successfully updated' + content: + application/json: + schema: + $ref: '#/components/schemas/PlexSettings' + /settings/plex/library: + get: + summary: Get Plex libraries + description: Returns a list of Plex libraries in a JSON array. + tags: + - settings + parameters: + - in: query + name: sync + description: Syncs the current libraries with the current Plex server + schema: + type: string + nullable: true + - in: query + name: enable + explode: false + allowReserved: true + description: Comma separated list of libraries to enable. Any libraries not passed will be disabled! + schema: + type: string + nullable: true + responses: + '200': + description: 'Plex libraries returned' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PlexLibrary' + /settings/plex/sync: + get: + summary: Get status of full Plex library scan + description: Returns scan progress in a JSON array. + tags: + - settings + responses: + '200': + description: Status of Plex scan + content: + application/json: + schema: + type: object + properties: + running: + type: boolean + example: false + progress: + type: number + example: 0 + total: + type: number + example: 100 + currentLibrary: + $ref: '#/components/schemas/PlexLibrary' + libraries: + type: array + items: + $ref: '#/components/schemas/PlexLibrary' + post: + summary: Start full Plex library scan + description: Runs a full Plex library scan and returns the progress in a JSON array. + tags: + - settings + requestBody: + content: + application/json: + schema: + type: object + properties: + cancel: + type: boolean + example: false + start: + type: boolean + example: false + responses: + '200': + description: Status of Plex scan + content: + application/json: + schema: + type: object + properties: + running: + type: boolean + example: false + progress: + type: number + example: 0 + total: + type: number + example: 100 + currentLibrary: + $ref: '#/components/schemas/PlexLibrary' + libraries: + type: array + items: + $ref: '#/components/schemas/PlexLibrary' + /settings/plex/devices/servers: + get: + summary: Gets the user's available Plex servers + description: Returns a list of available Plex servers and their connectivity state + tags: + - settings + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PlexDevice' + /settings/plex/users: + get: + summary: Get Plex users + description: | + Returns a list of Plex users in a JSON array. + + Requires the `MANAGE_USERS` permission. + tags: + - settings + - users + responses: + '200': + description: Plex users + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: string + title: + type: string + username: + type: string + email: + type: string + thumb: + type: string + /settings/tautulli: + get: + summary: Get Tautulli settings + description: Retrieves current Tautulli settings. + tags: + - settings + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/TautulliSettings' + post: + summary: Update Tautulli settings + description: Updates Tautulli settings with the provided values. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TautulliSettings' + responses: + '200': + description: 'Values were successfully updated' + content: + application/json: + schema: + $ref: '#/components/schemas/TautulliSettings' + /settings/radarr: + get: + summary: Get Radarr settings + description: Returns all Radarr settings in a JSON array. + tags: + - settings + responses: + '200': + description: 'Values were returned' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/RadarrSettings' + post: + summary: Create Radarr instance + description: Creates a new Radarr instance from the request body. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RadarrSettings' + responses: + '201': + description: 'New Radarr instance created' + content: + application/json: + schema: + $ref: '#/components/schemas/RadarrSettings' + /settings/radarr/test: + post: + summary: Test Radarr configuration + description: Tests if the Radarr configuration is valid. Returns profiles and root folders on success. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + hostname: + type: string + example: '127.0.0.1' + port: + type: number + example: 7878 + apiKey: + type: string + example: yourapikey + useSsl: + type: boolean + example: false + baseUrl: + type: string + required: + - hostname + - port + - apiKey + - useSsl + responses: + '200': + description: Succesfully connected to Radarr instance + content: + application/json: + schema: + type: object + properties: + profiles: + type: array + items: + $ref: '#/components/schemas/ServiceProfile' + /settings/radarr/{radarrId}: + put: + summary: Update Radarr instance + description: Updates an existing Radarr instance with the provided values. + tags: + - settings + parameters: + - in: path + name: radarrId + required: true + schema: + type: integer + description: Radarr instance ID + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RadarrSettings' + responses: + '200': + description: 'Radarr instance updated' + content: + application/json: + schema: + $ref: '#/components/schemas/RadarrSettings' + delete: + summary: Delete Radarr instance + description: Deletes an existing Radarr instance based on the radarrId parameter. + tags: + - settings + parameters: + - in: path + name: radarrId + required: true + schema: + type: integer + description: Radarr instance ID + responses: + '200': + description: 'Radarr instance updated' + content: + application/json: + schema: + $ref: '#/components/schemas/RadarrSettings' + /settings/radarr/{radarrId}/profiles: + get: + summary: Get available Radarr profiles + description: Returns a list of profiles available on the Radarr server instance in a JSON array. + tags: + - settings + parameters: + - in: path + name: radarrId + required: true + schema: + type: integer + description: Radarr instance ID + responses: + '200': + description: Returned list of profiles + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ServiceProfile' + /settings/sonarr: + get: + summary: Get Sonarr settings + description: Returns all Sonarr settings in a JSON array. + tags: + - settings + responses: + '200': + description: 'Values were returned' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/SonarrSettings' + post: + summary: Create Sonarr instance + description: Creates a new Sonarr instance from the request body. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SonarrSettings' + responses: + '201': + description: 'New Sonarr instance created' + content: + application/json: + schema: + $ref: '#/components/schemas/SonarrSettings' + /settings/sonarr/test: + post: + summary: Test Sonarr configuration + description: Tests if the Sonarr configuration is valid. Returns profiles and root folders on success. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + hostname: + type: string + example: '127.0.0.1' + port: + type: number + example: 8989 + apiKey: + type: string + example: yourapikey + useSsl: + type: boolean + example: false + baseUrl: + type: string + required: + - hostname + - port + - apiKey + - useSsl + responses: + '200': + description: Succesfully connected to Sonarr instance + content: + application/json: + schema: + type: object + properties: + profiles: + type: array + items: + $ref: '#/components/schemas/ServiceProfile' + /settings/sonarr/{sonarrId}: + put: + summary: Update Sonarr instance + description: Updates an existing Sonarr instance with the provided values. + tags: + - settings + parameters: + - in: path + name: sonarrId + required: true + schema: + type: integer + description: Sonarr instance ID + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SonarrSettings' + responses: + '200': + description: 'Sonarr instance updated' + content: + application/json: + schema: + $ref: '#/components/schemas/SonarrSettings' + delete: + summary: Delete Sonarr instance + description: Deletes an existing Sonarr instance based on the sonarrId parameter. + tags: + - settings + parameters: + - in: path + name: sonarrId + required: true + schema: + type: integer + description: Sonarr instance ID + responses: + '200': + description: 'Sonarr instance updated' + content: + application/json: + schema: + $ref: '#/components/schemas/SonarrSettings' + /settings/public: + get: + summary: Get public settings + security: [] + description: Returns settings that are not protected or sensitive. Mainly used to determine if the application has been configured for the first time. + tags: + - settings + responses: + '200': + description: Public settings returned + content: + application/json: + schema: + $ref: '#/components/schemas/PublicSettings' + /settings/initialize: + post: + summary: Initialize application + description: Sets the app as initialized, allowing the user to navigate to pages other than the setup page. + tags: + - settings + responses: + '200': + description: Public settings returned + content: + application/json: + schema: + $ref: '#/components/schemas/PublicSettings' + /settings/jobs: + get: + summary: Get scheduled jobs + description: Returns list of all scheduled jobs and details about their next execution time in a JSON array. + tags: + - settings + responses: + '200': + description: Scheduled jobs returned + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Job' + /settings/jobs/{jobId}/run: + post: + summary: Invoke a specific job + description: Invokes a specific job to run. Will return the new job status in JSON format. + tags: + - settings + parameters: + - in: path + name: jobId + required: true + schema: + type: string + responses: + '200': + description: Invoked job returned + content: + application/json: + schema: + $ref: '#/components/schemas/Job' + /settings/jobs/{jobId}/cancel: + post: + summary: Cancel a specific job + description: Cancels a specific job. Will return the new job status in JSON format. + tags: + - settings + parameters: + - in: path + name: jobId + required: true + schema: + type: string + responses: + '200': + description: Canceled job returned + content: + application/json: + schema: + $ref: '#/components/schemas/Job' + /settings/jobs/{jobId}/schedule: + post: + summary: Modify job schedule + description: Re-registers the job with the schedule specified. Will return the job in JSON format. + tags: + - settings + parameters: + - in: path + name: jobId + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + schedule: + type: string + example: '0 */5 * * * *' + responses: + '200': + description: Rescheduled job + content: + application/json: + schema: + $ref: '#/components/schemas/Job' + /settings/cache: + get: + summary: Get a list of active caches + description: Retrieves a list of all active caches and their current stats. + tags: + - settings + responses: + '200': + description: Caches returned + content: + application/json: + schema: + type: object + properties: + imageCache: + type: object + properties: + tmdb: + type: object + properties: + size: + type: number + example: 123456 + imageCount: + type: number + example: 123 + avatar: + type: object + properties: + size: + type: number + example: 123456 + imageCount: + type: number + example: 123 + apiCaches: + type: array + items: + type: object + properties: + id: + type: string + example: cache-id + name: + type: string + example: cache name + stats: + type: object + properties: + hits: + type: number + misses: + type: number + keys: + type: number + ksize: + type: number + vsize: + type: number + /settings/cache/{cacheId}/flush: + post: + summary: Flush a specific cache + description: Flushes all data from the cache ID provided + tags: + - settings + parameters: + - in: path + name: cacheId + required: true + schema: + type: string + responses: + '204': + description: 'Flushed cache' + /settings/logs: + get: + summary: Returns logs + description: Returns list of all log items and details + tags: + - settings + parameters: + - in: query + name: take + schema: + type: number + nullable: true + example: 25 + - in: query + name: skip + schema: + type: number + nullable: true + example: 0 + - in: query + name: filter + schema: + type: string + nullable: true + enum: [debug, info, warn, error] + default: debug + - in: query + name: search + schema: + type: string + nullable: true + example: plex + responses: + '200': + description: Server log returned + content: + application/json: + schema: + type: array + items: + type: object + properties: + label: + type: string + example: server + level: + type: string + example: info + message: + type: string + example: Server ready on port 5055 + timestamp: + type: string + example: '2020-12-15T16:20:00.069Z' + /settings/notifications/email: + get: + summary: Get email notification settings + description: Returns current email notification settings in a JSON object. + tags: + - settings + responses: + '200': + description: Returned email settings + content: + application/json: + schema: + $ref: '#/components/schemas/NotificationEmailSettings' + post: + summary: Update email notification settings + description: Updates email notification settings with provided values + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/NotificationEmailSettings' + responses: + '200': + description: 'Values were sucessfully updated' + content: + application/json: + schema: + $ref: '#/components/schemas/NotificationEmailSettings' + /settings/notifications/email/test: + post: + summary: Test email settings + description: Sends a test notification to the email agent. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/NotificationEmailSettings' + responses: + '204': + description: Test notification attempted + /settings/notifications/discord: + get: + summary: Get Discord notification settings + description: Returns current Discord notification settings in a JSON object. + tags: + - settings + responses: + '200': + description: Returned Discord settings + content: + application/json: + schema: + $ref: '#/components/schemas/DiscordSettings' + post: + summary: Update Discord notification settings + description: Updates Discord notification settings with the provided values. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DiscordSettings' + responses: + '200': + description: 'Values were sucessfully updated' + content: + application/json: + schema: + $ref: '#/components/schemas/DiscordSettings' + /settings/notifications/discord/test: + post: + summary: Test Discord settings + description: Sends a test notification to the Discord agent. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DiscordSettings' + responses: + '204': + description: Test notification attempted + /settings/notifications/lunasea: + get: + summary: Get LunaSea notification settings + description: Returns current LunaSea notification settings in a JSON object. + tags: + - settings + responses: + '200': + description: Returned LunaSea settings + content: + application/json: + schema: + $ref: '#/components/schemas/LunaSeaSettings' + post: + summary: Update LunaSea notification settings + description: Updates LunaSea notification settings with the provided values. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/LunaSeaSettings' + responses: + '200': + description: 'Values were sucessfully updated' + content: + application/json: + schema: + $ref: '#/components/schemas/LunaSeaSettings' + /settings/notifications/lunasea/test: + post: + summary: Test LunaSea settings + description: Sends a test notification to the LunaSea agent. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/LunaSeaSettings' + responses: + '204': + description: Test notification attempted + /settings/notifications/pushbullet: + get: + summary: Get Pushbullet notification settings + description: Returns current Pushbullet notification settings in a JSON object. + tags: + - settings + responses: + '200': + description: Returned Pushbullet settings + content: + application/json: + schema: + $ref: '#/components/schemas/PushbulletSettings' + post: + summary: Update Pushbullet notification settings + description: Update Pushbullet notification settings with the provided values. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PushbulletSettings' + responses: + '200': + description: 'Values were sucessfully updated' + content: + application/json: + schema: + $ref: '#/components/schemas/PushbulletSettings' + /settings/notifications/pushbullet/test: + post: + summary: Test Pushbullet settings + description: Sends a test notification to the Pushbullet agent. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PushbulletSettings' + responses: + '204': + description: Test notification attempted + /settings/notifications/pushover: + get: + summary: Get Pushover notification settings + description: Returns current Pushover notification settings in a JSON object. + tags: + - settings + responses: + '200': + description: Returned Pushover settings + content: + application/json: + schema: + $ref: '#/components/schemas/PushoverSettings' + post: + summary: Update Pushover notification settings + description: Update Pushover notification settings with the provided values. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PushoverSettings' + responses: + '200': + description: 'Values were sucessfully updated' + content: + application/json: + schema: + $ref: '#/components/schemas/PushoverSettings' + /settings/notifications/pushover/test: + post: + summary: Test Pushover settings + description: Sends a test notification to the Pushover agent. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PushoverSettings' + responses: + '204': + description: Test notification attempted + /settings/notifications/pushover/sounds: + get: + summary: Get Pushover sounds + description: Returns valid Pushover sound options in a JSON array. + tags: + - settings + parameters: + - in: query + name: token + required: true + schema: + type: string + nullable: false + responses: + '200': + description: Returned Pushover settings + content: + application/json: + schema: + type: array + items: + type: object + properties: + name: + type: string + description: + type: string + /settings/notifications/gotify: + get: + summary: Get Gotify notification settings + description: Returns current Gotify notification settings in a JSON object. + tags: + - settings + responses: + '200': + description: Returned Gotify settings + content: + application/json: + schema: + $ref: '#/components/schemas/GotifySettings' + post: + summary: Update Gotify notification settings + description: Update Gotify notification settings with the provided values. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GotifySettings' + responses: + '200': + description: 'Values were sucessfully updated' + content: + application/json: + schema: + $ref: '#/components/schemas/GotifySettings' + /settings/notifications/gotify/test: + post: + summary: Test Gotify settings + description: Sends a test notification to the Gotify agent. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GotifySettings' + responses: + '204': + description: Test notification attempted + /settings/notifications/ntfy: + get: + summary: Get ntfy.sh notification settings + description: Returns current ntfy.sh notification settings in a JSON object. + tags: + - settings + responses: + '200': + description: Returned ntfy.sh settings + content: + application/json: + schema: + $ref: '#/components/schemas/NtfySettings' + post: + summary: Update ntfy.sh notification settings + description: Update ntfy.sh notification settings with the provided values. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/NtfySettings' + responses: + '200': + description: 'Values were sucessfully updated' + content: + application/json: + schema: + $ref: '#/components/schemas/NtfySettings' + /settings/notifications/ntfy/test: + post: + summary: Test ntfy.sh settings + description: Sends a test notification to the ntfy.sh agent. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/NtfySettings' + responses: + '204': + description: Test notification attempted + /settings/notifications/slack: + get: + summary: Get Slack notification settings + description: Returns current Slack notification settings in a JSON object. + tags: + - settings + responses: + '200': + description: Returned slack settings + content: + application/json: + schema: + $ref: '#/components/schemas/SlackSettings' + post: + summary: Update Slack notification settings + description: Updates Slack notification settings with the provided values. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SlackSettings' + responses: + '200': + description: 'Values were sucessfully updated' + content: + application/json: + schema: + $ref: '#/components/schemas/SlackSettings' + /settings/notifications/slack/test: + post: + summary: Test Slack settings + description: Sends a test notification to the Slack agent. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SlackSettings' + responses: + '204': + description: Test notification attempted + /settings/notifications/telegram: + get: + summary: Get Telegram notification settings + description: Returns current Telegram notification settings in a JSON object. + tags: + - settings + responses: + '200': + description: Returned Telegram settings + content: + application/json: + schema: + $ref: '#/components/schemas/TelegramSettings' + post: + summary: Update Telegram notification settings + description: Update Telegram notification settings with the provided values. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TelegramSettings' + responses: + '200': + description: 'Values were sucessfully updated' + content: + application/json: + schema: + $ref: '#/components/schemas/TelegramSettings' + /settings/notifications/telegram/test: + post: + summary: Test Telegram settings + description: Sends a test notification to the Telegram agent. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TelegramSettings' + responses: + '204': + description: Test notification attempted + /settings/notifications/webpush: + get: + summary: Get Web Push notification settings + description: Returns current Web Push notification settings in a JSON object. + tags: + - settings + responses: + '200': + description: Returned web push settings + content: + application/json: + schema: + $ref: '#/components/schemas/WebPushSettings' + post: + summary: Update Web Push notification settings + description: Updates Web Push notification settings with the provided values. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WebPushSettings' + responses: + '200': + description: 'Values were sucessfully updated' + content: + application/json: + schema: + $ref: '#/components/schemas/WebPushSettings' + /settings/notifications/webpush/test: + post: + summary: Test Web Push settings + description: Sends a test notification to the Web Push agent. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WebPushSettings' + responses: + '204': + description: Test notification attempted + /settings/notifications/webhook: + get: + summary: Get webhook notification settings + description: Returns current webhook notification settings in a JSON object. + tags: + - settings + responses: + '200': + description: Returned webhook settings + content: + application/json: + schema: + $ref: '#/components/schemas/WebhookSettings' + post: + summary: Update webhook notification settings + description: Updates webhook notification settings with the provided values. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WebhookSettings' + responses: + '200': + description: 'Values were sucessfully updated' + content: + application/json: + schema: + $ref: '#/components/schemas/WebhookSettings' + /settings/notifications/webhook/test: + post: + summary: Test webhook settings + description: Sends a test notification to the webhook agent. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WebhookSettings' + responses: + '204': + description: Test notification attempted + /settings/discover: + get: + summary: Get all discover sliders + description: Returns all discovery sliders. Built-in and custom made. + tags: + - settings + responses: + '200': + description: Returned all discovery sliders + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/DiscoverSlider' + post: + summary: Batch update all sliders. + description: | + Batch update all sliders at once. Should also be used for creation. Will only update sliders provided + and will not delete any sliders not present in the request. If a slider is missing a required field, + it will be ignored. Requires the `ADMIN` permission. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/DiscoverSlider' + responses: + '200': + description: Returned all newly updated discovery sliders + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/DiscoverSlider' + /settings/discover/{sliderId}: + put: + summary: Update a single slider + description: | + Updates a single slider and return the newly updated slider. Requires the `ADMIN` permission. + tags: + - settings + parameters: + - in: path + name: sliderId + required: true + schema: + type: number + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + title: + type: string + example: 'Slider Title' + type: + type: number + example: 1 + data: + type: string + example: '1' + responses: + '200': + description: Returns newly added discovery slider + content: + application/json: + schema: + $ref: '#/components/schemas/DiscoverSlider' + delete: + summary: Delete slider by ID + description: Deletes the slider with the provided sliderId. Requires the `ADMIN` permission. + tags: + - settings + parameters: + - in: path + name: sliderId + required: true + schema: + type: number + responses: + '200': + description: Slider successfully deleted + content: + application/json: + schema: + $ref: '#/components/schemas/DiscoverSlider' + /settings/discover/add: + post: + summary: Add a new slider + description: | + Add a single slider and return the newly created slider. Requires the `ADMIN` permission. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + title: + type: string + example: 'New Slider' + type: + type: number + example: 1 + data: + type: string + example: '1' + responses: + '200': + description: Returns newly added discovery slider + content: + application/json: + schema: + $ref: '#/components/schemas/DiscoverSlider' + /settings/discover/reset: + get: + summary: Reset all discover sliders + description: Resets all discovery sliders to the default values. Requires the `ADMIN` permission. + tags: + - settings + responses: + '204': + description: All sliders reset to defaults + /settings/about: + get: + summary: Get server stats + description: Returns current server stats in a JSON object. + tags: + - settings + responses: + '200': + description: Returned about settings + content: + application/json: + schema: + type: object + properties: + version: + type: string + example: '1.0.0' + totalRequests: + type: number + example: 100 + totalMediaItems: + type: number + example: 100 + tz: + type: string + nullable: true + example: Asia/Tokyo + appDataPath: + type: string + example: /app/config + /auth/me: + get: + summary: Get logged-in user + description: Returns the currently logged-in user. + tags: + - auth + - users + responses: + '200': + description: Object containing the logged-in user in JSON + content: + application/json: + schema: + $ref: '#/components/schemas/User' + /auth/plex: + post: + summary: Sign in using a Plex token + description: Takes an `authToken` (Plex token) to log the user in. Generates a session cookie for use in further requests. If the user does not exist, and there are no other users, then a user will be created with full admin privileges. If a user logs in with access to the main Plex server, they will also have an account created, but without any permissions. + security: [] + tags: + - auth + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/User' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + authToken: + type: string + required: + - authToken + /auth/jellyfin: + post: + summary: Sign in using a Jellyfin username and password + description: Takes the user's username and password to log the user in. Generates a session cookie for use in further requests. If the user does not exist, and there are no other users, then a user will be created with full admin privileges. If a user logs in with access to the Jellyfin server, they will also have an account created, but without any permissions. + security: [] + tags: + - auth + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/User' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + username: + type: string + password: + type: string + hostname: + type: string + email: + type: string + serverType: + type: number + required: + - username + - password + /auth/local: + post: + summary: Sign in using a local account + description: Takes an `email` and a `password` to log the user in. Generates a session cookie for use in further requests. + security: [] + tags: + - auth + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/User' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + email: + type: string + password: + type: string + required: + - email + - password + /auth/logout: + post: + summary: Sign out and clear session cookie + description: Completely clear the session cookie and associated values, effectively signing the user out. + tags: + - auth + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: 'ok' + /auth/reset-password: + post: + summary: Send a reset password email + description: Sends a reset password email to the email if the user exists + security: [] + tags: + - users + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: 'ok' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + email: + type: string + required: + - email + /auth/reset-password/{guid}: + post: + summary: Reset the password for a user + description: Resets the password for a user if the given guid is connected to a user + security: [] + tags: + - users + parameters: + - in: path + name: guid + required: true + schema: + type: string + example: '9afef5a7-ec89-4d5f-9397-261e96970b50' + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: 'ok' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + password: + type: string + required: + - password + /user: + get: + summary: Get all users + description: Returns all users in a JSON object. + tags: + - users + parameters: + - in: query + name: take + schema: + type: number + nullable: true + example: 20 + - in: query + name: skip + schema: + type: number + nullable: true + example: 0 + - in: query + name: sort + schema: + type: string + enum: [created, updated, requests, displayname] + default: created + - in: query + name: q + required: false + schema: + type: string + - in: query + name: includeIds + required: false + schema: + type: string + responses: + '200': + description: A JSON array of all users + content: + application/json: + schema: + type: object + properties: + pageInfo: + $ref: '#/components/schemas/PageInfo' + results: + type: array + items: + $ref: '#/components/schemas/User' + post: + summary: Create new user + description: | + Creates a new user. Requires the `MANAGE_USERS` permission. + tags: + - users + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + email: + type: string + example: 'hey@itsme.com' + username: + type: string + permissions: + type: number + responses: + '201': + description: The created user + content: + application/json: + schema: + $ref: '#/components/schemas/User' + put: + summary: Update batch of users + description: | + Update users with given IDs with provided values in request `body.settings`. You cannot update users' Plex tokens through this request. + + Requires the `MANAGE_USERS` permission. + tags: + - users + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + ids: + type: array + items: + type: integer + permissions: + type: integer + responses: + '200': + description: Successfully updated user details + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + /user/import-from-plex: + post: + summary: Import all users from Plex + description: | + Fetches and imports users from the Plex server. If a list of Plex IDs is provided in the request body, only the specified users will be imported. Otherwise, all users will be imported. + + Requires the `MANAGE_USERS` permission. + tags: + - users + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + plexIds: + type: array + items: + type: string + responses: + '201': + description: A list of the newly created users + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + /user/import-from-jellyfin: + post: + summary: Import all users from Jellyfin + description: | + Fetches and imports users from the Jellyfin server. + + Requires the `MANAGE_USERS` permission. + tags: + - users + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + jellyfinUserIds: + type: array + items: + type: string + responses: + '201': + description: A list of the newly created users + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + /user/registerPushSubscription: + post: + summary: Register a web push /user/registerPushSubscription + description: Registers a web push subscription for the logged-in user + tags: + - users + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + endpoint: + type: string + auth: + type: string + p256dh: + type: string + userAgent: + type: string + required: + - endpoint + - auth + - p256dh + responses: + '204': + description: Successfully registered push subscription + /user/{userId}/pushSubscriptions: + get: + summary: Get all web push notification settings for a user + description: | + Returns all web push notification settings for a user in a JSON object. + tags: + - users + parameters: + - in: path + name: userId + required: true + schema: + type: number + responses: + '200': + description: User web push notification settings in JSON + content: + application/json: + schema: + type: object + properties: + endpoint: + type: string + p256dh: + type: string + auth: + type: string + userAgent: + type: string + /user/{userId}/pushSubscription/{key}: + get: + summary: Get web push notification settings for a user + description: | + Returns web push notification settings for a user in a JSON object. + tags: + - users + parameters: + - in: path + name: userId + required: true + schema: + type: number + - in: path + name: key + required: true + schema: + type: string + responses: + '200': + description: User web push notification settings in JSON + content: + application/json: + schema: + type: object + properties: + endpoint: + type: string + p256dh: + type: string + auth: + type: string + userAgent: + type: string + delete: + summary: Delete user push subscription by key + description: Deletes the user push subscription with the provided key. + tags: + - users + parameters: + - in: path + name: userId + required: true + schema: + type: number + - in: path + name: key + required: true + schema: + type: string + responses: + '204': + description: Successfully removed user push subscription + /user/{userId}: + get: + summary: Get user by ID + description: | + Retrieves user details in a JSON object. Requires the `MANAGE_USERS` permission. + tags: + - users + parameters: + - in: path + name: userId + required: true + schema: + type: number + responses: + '200': + description: Users details in JSON + content: + application/json: + schema: + $ref: '#/components/schemas/User' + put: + summary: Update a user by user ID + description: | + Update a user with the provided values. You cannot update a user's Plex token through this request. + + Requires the `MANAGE_USERS` permission. + tags: + - users + parameters: + - in: path + name: userId + required: true + schema: + type: number + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/User' + responses: + '200': + description: Successfully updated user details + content: + application/json: + schema: + $ref: '#/components/schemas/User' + delete: + summary: Delete user by ID + description: Deletes the user with the provided userId. Requires the `MANAGE_USERS` permission. + tags: + - users + parameters: + - in: path + name: userId + required: true + schema: + type: number + responses: + '200': + description: User successfully deleted + content: + application/json: + schema: + $ref: '#/components/schemas/User' + /user/{userId}/requests: + get: + summary: Get requests for a specific user + description: | + Retrieves a user's requests in a JSON object. + tags: + - users + parameters: + - in: path + name: userId + required: true + schema: + type: number + - in: query + name: take + schema: + type: number + nullable: true + example: 20 + - in: query + name: skip + schema: + type: number + nullable: true + example: 0 + responses: + '200': + description: User's requests returned + content: + application/json: + schema: + type: object + properties: + pageInfo: + $ref: '#/components/schemas/PageInfo' + results: + type: array + items: + $ref: '#/components/schemas/MediaRequest' + /user/{userId}/quota: + get: + summary: Get quotas for a specific user + description: | + Returns quota details for a user in a JSON object. Requires `MANAGE_USERS` permission if viewing other users. + tags: + - users + parameters: + - in: path + name: userId + required: true + schema: + type: number + responses: + '200': + description: User quota details in JSON + content: + application/json: + schema: + type: object + properties: + movie: + type: object + properties: + days: + type: number + example: 7 + limit: + type: number + example: 10 + used: + type: number + example: 6 + remaining: + type: number + example: 4 + restricted: + type: boolean + example: false + tv: + type: object + properties: + days: + type: number + example: 7 + limit: + type: number + example: 10 + used: + type: number + example: 6 + remaining: + type: number + example: 4 + restricted: + type: boolean + example: false + /blacklist: + get: + summary: Returns blacklisted items + description: Returns list of all blacklisted media + tags: + - settings + parameters: + - in: query + name: take + schema: + type: number + nullable: true + example: 25 + - in: query + name: skip + schema: + type: number + nullable: true + example: 0 + - in: query + name: search + schema: + type: string + nullable: true + example: dune + - in: query + name: filter + schema: + type: string + enum: [all, manual, blacklistedTags] + default: manual + responses: + '200': + description: Blacklisted items returned + content: + application/json: + schema: + type: object + properties: + pageInfo: + $ref: '#/components/schemas/PageInfo' + results: + type: array + items: + type: object + properties: + user: + $ref: '#/components/schemas/User' + createdAt: + type: string + example: 2024-04-21T01:55:44.000Z + id: + type: number + example: 1 + mediaType: + type: string + example: movie + title: + type: string + example: Dune + tmdbId: + type: number + example: 438631 + post: + summary: Add media to blacklist + tags: + - blacklist + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Blacklist' + responses: + '201': + description: Item succesfully blacklisted + '412': + description: Item has already been blacklisted + /blacklist/{tmdbId}: + get: + summary: Get media from blacklist + tags: + - blacklist + parameters: + - in: path + name: tmdbId + description: tmdbId ID + required: true + example: '1' + schema: + type: string + responses: + '200': + description: Blacklist details in JSON + delete: + summary: Remove media from blacklist + tags: + - blacklist + parameters: + - in: path + name: tmdbId + description: tmdbId ID + required: true + example: '1' + schema: + type: string + responses: + '204': + description: Succesfully removed media item + /watchlist: + post: + summary: Add media to watchlist + tags: + - watchlist + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Watchlist' + responses: + '200': + description: Watchlist data returned + content: + application/json: + schema: + $ref: '#/components/schemas/Watchlist' + /watchlist/{tmdbId}: + delete: + summary: Delete watchlist item + description: Removes a watchlist item. + tags: + - watchlist + parameters: + - in: path + name: tmdbId + description: tmdbId ID + required: true + example: '1' + schema: + type: string + responses: + '204': + description: Succesfully removed watchlist item + /user/{userId}/watchlist: + get: + summary: Get the Plex watchlist for a specific user + description: | + Retrieves a user's Plex Watchlist in a JSON object. + tags: + - users + - watchlist + parameters: + - in: path + name: userId + required: true + schema: + type: number + - in: query + name: page + schema: + type: number + example: 1 + default: 1 + responses: + '200': + description: Watchlist data returned + content: + application/json: + schema: + type: object + properties: + page: + type: number + totalPages: + type: number + totalResults: + type: number + results: + type: array + items: + type: object + properties: + tmdbId: + type: number + example: 1 + ratingKey: + type: string + type: + type: string + title: + type: string + /user/{userId}/settings/main: + get: + summary: Get general settings for a user + description: Returns general settings for a specific user. Requires `MANAGE_USERS` permission if viewing other users. + tags: + - users + parameters: + - in: path + name: userId + required: true + schema: + type: number + responses: + '200': + description: User general settings returned + content: + application/json: + schema: + type: object + properties: + username: + type: string + example: 'Mr User' + post: + summary: Update general settings for a user + description: Updates and returns general settings for a specific user. Requires `MANAGE_USERS` permission if editing other users. + tags: + - users + parameters: + - in: path + name: userId + required: true + schema: + type: number + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + username: + type: string + nullable: true + responses: + '200': + description: Updated user general settings returned + content: + application/json: + schema: + type: object + properties: + username: + type: string + example: 'Mr User' + /user/{userId}/settings/password: + get: + summary: Get password page informatiom + description: Returns important data for the password page to function correctly. Requires `MANAGE_USERS` permission if viewing other users. + tags: + - users + parameters: + - in: path + name: userId + required: true + schema: + type: number + responses: + '200': + description: User password page information returned + content: + application/json: + schema: + type: object + properties: + hasPassword: + type: boolean + example: true + post: + summary: Update password for a user + description: Updates a user's password. Requires `MANAGE_USERS` permission if editing other users. + tags: + - users + parameters: + - in: path + name: userId + required: true + schema: + type: number + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + currentPassword: + type: string + nullable: true + newPassword: + type: string + required: + - newPassword + responses: + '204': + description: User password updated + /user/{userId}/settings/linked-accounts/plex: + post: + summary: Link the provided Plex account to the current user + description: Logs in to Plex with the provided auth token, then links the associated Plex account with the user's account. Users can only link external accounts to their own account. + tags: + - users + parameters: + - in: path + name: userId + required: true + schema: + type: number + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + authToken: + type: string + required: + - authToken + responses: + '204': + description: Linking account succeeded + '403': + description: Invalid credentials + '422': + description: Account already linked to a user + delete: + summary: Remove the linked Plex account for a user + description: Removes the linked Plex account for a specific user. Requires `MANAGE_USERS` permission if editing other users. + tags: + - users + parameters: + - in: path + name: userId + required: true + schema: + type: number + responses: + '204': + description: Unlinking account succeeded + '400': + description: Unlink request invalid + '404': + description: User does not exist + /user/{userId}/settings/linked-accounts/jellyfin: + post: + summary: Link the provided Jellyfin account to the current user + description: Logs in to Jellyfin with the provided credentials, then links the associated Jellyfin account with the user's account. Users can only link external accounts to their own account. + tags: + - users + parameters: + - in: path + name: userId + required: true + schema: + type: number + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + username: + type: string + example: 'Mr User' + password: + type: string + example: 'supersecret' + responses: + '204': + description: Linking account succeeded + '403': + description: Invalid credentials + '422': + description: Account already linked to a user + delete: + summary: Remove the linked Jellyfin account for a user + description: Removes the linked Jellyfin account for a specific user. Requires `MANAGE_USERS` permission if editing other users. + tags: + - users + parameters: + - in: path + name: userId + required: true + schema: + type: number + responses: + '204': + description: Unlinking account succeeded + '400': + description: Unlink request invalid + '404': + description: User does not exist + /user/{userId}/settings/notifications: + get: + summary: Get notification settings for a user + description: Returns notification settings for a specific user. Requires `MANAGE_USERS` permission if viewing other users. + tags: + - users + parameters: + - in: path + name: userId + required: true + schema: + type: number + responses: + '200': + description: User notification settings returned + content: + application/json: + schema: + $ref: '#/components/schemas/UserSettingsNotifications' + post: + summary: Update notification settings for a user + description: Updates and returns notification settings for a specific user. Requires `MANAGE_USERS` permission if editing other users. + tags: + - users + parameters: + - in: path + name: userId + required: true + schema: + type: number + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UserSettingsNotifications' + responses: + '200': + description: Updated user notification settings returned + content: + application/json: + schema: + $ref: '#/components/schemas/UserSettingsNotifications' + /user/{userId}/settings/permissions: + get: + summary: Get permission settings for a user + description: Returns permission settings for a specific user. Requires `MANAGE_USERS` permission if viewing other users. + tags: + - users + parameters: + - in: path + name: userId + required: true + schema: + type: number + responses: + '200': + description: User permission settings returned + content: + application/json: + schema: + type: object + properties: + permissions: + type: number + example: 2 + post: + summary: Update permission settings for a user + description: Updates and returns permission settings for a specific user. Requires `MANAGE_USERS` permission if editing other users. + tags: + - users + parameters: + - in: path + name: userId + required: true + schema: + type: number + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + permissions: + type: number + required: + - permissions + responses: + '200': + description: Updated user general settings returned + content: + application/json: + schema: + type: object + properties: + permissions: + type: number + example: 2 + /user/{userId}/watch_data: + get: + summary: Get watch data + description: | + Returns play count, play duration, and recently watched media. + + Requires the `ADMIN` permission to fetch results for other users. + tags: + - users + parameters: + - in: path + name: userId + required: true + schema: + type: number + responses: + '200': + description: Users + content: + application/json: + schema: + type: object + properties: + recentlyWatched: + type: array + items: + $ref: '#/components/schemas/MediaInfo' + playCount: + type: number + /search: + get: + summary: Search for movies, TV shows, or people + description: Returns a list of movies, TV shows, or people a JSON object. + tags: + - search + parameters: + - in: query + name: query + required: true + schema: + type: string + example: 'Mulan' + - in: query + name: page + schema: + type: number + example: 1 + default: 1 + - in: query + name: language + schema: + type: string + example: en + responses: + '200': + description: Results + content: + application/json: + schema: + type: object + properties: + page: + type: number + example: 1 + totalPages: + type: number + example: 20 + totalResults: + type: number + example: 200 + results: + type: array + items: + anyOf: + - $ref: '#/components/schemas/MovieResult' + - $ref: '#/components/schemas/TvResult' + - $ref: '#/components/schemas/PersonResult' + /search/keyword: + get: + summary: Search for keywords + description: Returns a list of TMDB keywords matching the search query + tags: + - search + parameters: + - in: query + name: query + required: true + schema: + type: string + example: 'christmas' + - in: query + name: page + schema: + type: number + example: 1 + default: 1 + responses: + '200': + description: Results + content: + application/json: + schema: + type: object + properties: + page: + type: number + example: 1 + totalPages: + type: number + example: 20 + totalResults: + type: number + example: 200 + results: + type: array + items: + $ref: '#/components/schemas/Keyword' + /search/company: + get: + summary: Search for companies + description: Returns a list of TMDB companies matching the search query. (Will not return origin country) + tags: + - search + parameters: + - in: query + name: query + required: true + schema: + type: string + example: 'Disney' + - in: query + name: page + schema: + type: number + example: 1 + default: 1 + responses: + '200': + description: Results + content: + application/json: + schema: + type: object + properties: + page: + type: number + example: 1 + totalPages: + type: number + example: 20 + totalResults: + type: number + example: 200 + results: + type: array + items: + $ref: '#/components/schemas/Company' + /discover/movies: + get: + summary: Discover movies + description: Returns a list of movies in a JSON object. + tags: + - search + parameters: + - in: query + name: page + schema: + type: number + example: 1 + default: 1 + - in: query + name: language + schema: + type: string + example: en + - in: query + name: genre + schema: + type: string + example: 18 + - in: query + name: studio + schema: + type: number + example: 1 + - in: query + name: keywords + schema: + type: string + example: 1,2 + - in: query + name: sortBy + schema: + type: string + example: popularity.desc + - in: query + name: primaryReleaseDateGte + schema: + type: string + example: 2022-01-01 + - in: query + name: primaryReleaseDateLte + schema: + type: string + example: 2023-01-01 + - in: query + name: withRuntimeGte + schema: + type: number + example: 60 + - in: query + name: withRuntimeLte + schema: + type: number + example: 120 + - in: query + name: voteAverageGte + schema: + type: number + example: 7 + - in: query + name: voteAverageLte + schema: + type: number + example: 10 + - in: query + name: voteCountGte + schema: + type: number + example: 7 + - in: query + name: voteCountLte + schema: + type: number + example: 10 + - in: query + name: watchRegion + schema: + type: string + example: US + - in: query + name: watchProviders + schema: + type: string + example: 8|9 + - in: query + name: certification + schema: + type: string + example: PG-13 + description: Exact certification to filter by (used when certificationMode is 'exact') + - in: query + name: certificationGte + schema: + type: string + example: G + description: Minimum certification to filter by (used when certificationMode is 'range') + - in: query + name: certificationLte + schema: + type: string + example: PG-13 + description: Maximum certification to filter by (used when certificationMode is 'range') + - in: query + name: certificationCountry + schema: + type: string + example: US + description: Country code for the certification system (e.g., US, GB, CA) + - in: query + name: certificationMode + schema: + type: string + enum: [exact, range] + example: exact + description: Determines whether to use exact certification matching or a certification range (internal use only, not sent to TMDB API) + responses: + '200': + description: Results + content: + application/json: + schema: + type: object + properties: + page: + type: number + example: 1 + totalPages: + type: number + example: 20 + totalResults: + type: number + example: 200 + results: + type: array + items: + $ref: '#/components/schemas/MovieResult' + /discover/movies/genre/{genreId}: + get: + summary: Discover movies by genre + description: Returns a list of movies based on the provided genre ID in a JSON object. + tags: + - search + parameters: + - in: path + name: genreId + required: true + schema: + type: string + example: '1' + - in: query + name: page + schema: + type: number + example: 1 + default: 1 + - in: query + name: language + schema: + type: string + example: en + responses: + '200': + description: Results + content: + application/json: + schema: + type: object + properties: + page: + type: number + example: 1 + totalPages: + type: number + example: 20 + totalResults: + type: number + example: 200 + genre: + $ref: '#/components/schemas/Genre' + results: + type: array + items: + $ref: '#/components/schemas/MovieResult' + /discover/movies/language/{language}: + get: + summary: Discover movies by original language + description: Returns a list of movies based on the provided ISO 639-1 language code in a JSON object. + tags: + - search + parameters: + - in: path + name: language + required: true + schema: + type: string + example: en + - in: query + name: page + schema: + type: number + example: 1 + default: 1 + - in: query + name: language + schema: + type: string + example: en + responses: + '200': + description: Results + content: + application/json: + schema: + type: object + properties: + page: + type: number + example: 1 + totalPages: + type: number + example: 20 + totalResults: + type: number + example: 200 + language: + $ref: '#/components/schemas/SpokenLanguage' + results: + type: array + items: + $ref: '#/components/schemas/MovieResult' + /discover/movies/studio/{studioId}: + get: + summary: Discover movies by studio + description: Returns a list of movies based on the provided studio ID in a JSON object. + tags: + - search + parameters: + - in: path + name: studioId + required: true + schema: + type: string + example: '1' + - in: query + name: page + schema: + type: number + example: 1 + default: 1 + - in: query + name: language + schema: + type: string + example: en + responses: + '200': + description: Results + content: + application/json: + schema: + type: object + properties: + page: + type: number + example: 1 + totalPages: + type: number + example: 20 + totalResults: + type: number + example: 200 + studio: + $ref: '#/components/schemas/ProductionCompany' + results: + type: array + items: + $ref: '#/components/schemas/MovieResult' + /discover/movies/upcoming: + get: + summary: Upcoming movies + description: Returns a list of movies in a JSON object. + tags: + - search + parameters: + - in: query + name: page + schema: + type: number + example: 1 + default: 1 + - in: query + name: language + schema: + type: string + example: en + responses: + '200': + description: Results + content: + application/json: + schema: + type: object + properties: + page: + type: number + example: 1 + totalPages: + type: number + example: 20 + totalResults: + type: number + example: 200 + results: + type: array + items: + $ref: '#/components/schemas/MovieResult' + /discover/tv: + get: + summary: Discover TV shows + description: Returns a list of TV shows in a JSON object. + tags: + - search + parameters: + - in: query + name: page + schema: + type: number + example: 1 + default: 1 + - in: query + name: language + schema: + type: string + example: en + - in: query + name: genre + schema: + type: string + example: 18 + - in: query + name: network + schema: + type: number + example: 1 + - in: query + name: keywords + schema: + type: string + example: 1,2 + - in: query + name: sortBy + schema: + type: string + example: popularity.desc + - in: query + name: firstAirDateGte + schema: + type: string + example: 2022-01-01 + - in: query + name: firstAirDateLte + schema: + type: string + example: 2023-01-01 + - in: query + name: withRuntimeGte + schema: + type: number + example: 60 + - in: query + name: withRuntimeLte + schema: + type: number + example: 120 + - in: query + name: voteAverageGte + schema: + type: number + example: 7 + - in: query + name: voteAverageLte + schema: + type: number + example: 10 + - in: query + name: voteCountGte + schema: + type: number + example: 7 + - in: query + name: voteCountLte + schema: + type: number + example: 10 + - in: query + name: watchRegion + schema: + type: string + example: US + - in: query + name: watchProviders + schema: + type: string + example: 8|9 + - in: query + name: status + schema: + type: string + example: 3|4 + - in: query + name: certification + schema: + type: string + example: TV-14 + description: Exact certification to filter by (used when certificationMode is 'exact') + - in: query + name: certificationGte + schema: + type: string + example: TV-PG + description: Minimum certification to filter by (used when certificationMode is 'range') + - in: query + name: certificationLte + schema: + type: string + example: TV-MA + description: Maximum certification to filter by (used when certificationMode is 'range') + - in: query + name: certificationCountry + schema: + type: string + example: US + description: Country code for the certification system (e.g., US, GB, CA) + - in: query + name: certificationMode + schema: + type: string + enum: [exact, range] + example: exact + description: Determines whether to use exact certification matching or a certification range (internal use only, not sent to TMDB API) + responses: + '200': + description: Results + content: + application/json: + schema: + type: object + properties: + page: + type: number + example: 1 + totalPages: + type: number + example: 20 + totalResults: + type: number + example: 200 + results: + type: array + items: + $ref: '#/components/schemas/TvResult' + /discover/tv/language/{language}: + get: + summary: Discover TV shows by original language + description: Returns a list of TV shows based on the provided ISO 639-1 language code in a JSON object. + tags: + - search + parameters: + - in: path + name: language + required: true + schema: + type: string + example: en + - in: query + name: page + schema: + type: number + example: 1 + default: 1 + - in: query + name: language + schema: + type: string + example: en + responses: + '200': + description: Results + content: + application/json: + schema: + type: object + properties: + page: + type: number + example: 1 + totalPages: + type: number + example: 20 + totalResults: + type: number + example: 200 + language: + $ref: '#/components/schemas/SpokenLanguage' + results: + type: array + items: + $ref: '#/components/schemas/TvResult' + /discover/tv/genre/{genreId}: + get: + summary: Discover TV shows by genre + description: Returns a list of TV shows based on the provided genre ID in a JSON object. + tags: + - search + parameters: + - in: path + name: genreId + required: true + schema: + type: string + example: '1' + - in: query + name: page + schema: + type: number + example: 1 + default: 1 + - in: query + name: language + schema: + type: string + example: en + responses: + '200': + description: Results + content: + application/json: + schema: + type: object + properties: + page: + type: number + example: 1 + totalPages: + type: number + example: 20 + totalResults: + type: number + example: 200 + genre: + $ref: '#/components/schemas/Genre' + results: + type: array + items: + $ref: '#/components/schemas/TvResult' + /discover/tv/network/{networkId}: + get: + summary: Discover TV shows by network + description: Returns a list of TV shows based on the provided network ID in a JSON object. + tags: + - search + parameters: + - in: path + name: networkId + required: true + schema: + type: string + example: '1' + - in: query + name: page + schema: + type: number + example: 1 + default: 1 + - in: query + name: language + schema: + type: string + example: en + responses: + '200': + description: Results + content: + application/json: + schema: + type: object + properties: + page: + type: number + example: 1 + totalPages: + type: number + example: 20 + totalResults: + type: number + example: 200 + network: + $ref: '#/components/schemas/Network' + results: + type: array + items: + $ref: '#/components/schemas/TvResult' + /discover/tv/upcoming: + get: + summary: Discover Upcoming TV shows + description: Returns a list of upcoming TV shows in a JSON object. + tags: + - search + parameters: + - in: query + name: page + schema: + type: number + example: 1 + default: 1 + - in: query + name: language + schema: + type: string + example: en + responses: + '200': + description: Results + content: + application/json: + schema: + type: object + properties: + page: + type: number + example: 1 + totalPages: + type: number + example: 20 + totalResults: + type: number + example: 200 + results: + type: array + items: + $ref: '#/components/schemas/TvResult' + /discover/trending: + get: + summary: Trending movies and TV + description: Returns a list of movies and TV shows in a JSON object. + tags: + - search + parameters: + - in: query + name: page + schema: + type: number + example: 1 + default: 1 + - in: query + name: language + schema: + type: string + example: en + responses: + '200': + description: Results + content: + application/json: + schema: + type: object + properties: + page: + type: number + example: 1 + totalPages: + type: number + example: 20 + totalResults: + type: number + example: 200 + results: + type: array + items: + anyOf: + - $ref: '#/components/schemas/MovieResult' + - $ref: '#/components/schemas/TvResult' + - $ref: '#/components/schemas/PersonResult' + /discover/keyword/{keywordId}/movies: + get: + summary: Get movies from keyword + description: Returns list of movies based on the provided keyword ID a JSON object. + tags: + - search + parameters: + - in: path + name: keywordId + required: true + schema: + type: number + example: 207317 + - in: query + name: page + schema: + type: number + example: 1 + default: 1 + - in: query + name: language + schema: + type: string + example: en + responses: + '200': + description: List of movies + content: + application/json: + schema: + type: object + properties: + page: + type: number + example: 1 + totalPages: + type: number + example: 20 + totalResults: + type: number + example: 200 + results: + type: array + items: + $ref: '#/components/schemas/MovieResult' + /discover/genreslider/movie: + get: + summary: Get genre slider data for movies + description: Returns a list of genres with backdrops attached + tags: + - search + parameters: + - in: query + name: language + schema: + type: string + example: en + responses: + '200': + description: Genre slider data returned + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: number + example: 1 + backdrops: + type: array + items: + type: string + name: + type: string + example: Genre Name + /discover/genreslider/tv: + get: + summary: Get genre slider data for TV series + description: Returns a list of genres with backdrops attached + tags: + - search + parameters: + - in: query + name: language + schema: + type: string + example: en + responses: + '200': + description: Genre slider data returned + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: number + example: 1 + backdrops: + type: array + items: + type: string + name: + type: string + example: Genre Name + /discover/watchlist: + get: + summary: Get the Plex watchlist. + tags: + - search + parameters: + - in: query + name: page + schema: + type: number + example: 1 + default: 1 + responses: + '200': + description: Watchlist data returned + content: + application/json: + schema: + type: object + properties: + page: + type: number + totalPages: + type: number + totalResults: + type: number + results: + type: array + items: + type: object + properties: + tmdbId: + type: number + example: 1 + ratingKey: + type: string + type: + type: string + title: + type: string + /request: + get: + summary: Get all requests + description: | + Returns all requests if the user has the `ADMIN` or `MANAGE_REQUESTS` permissions. Otherwise, only the logged-in user's requests are returned. + + If the `requestedBy` parameter is specified, only requests from that particular user ID will be returned. + tags: + - request + parameters: + - in: query + name: take + schema: + type: number + nullable: true + example: 20 + - in: query + name: skip + schema: + type: number + nullable: true + example: 0 + - in: query + name: filter + schema: + type: string + nullable: true + enum: + [ + all, + approved, + available, + pending, + processing, + unavailable, + failed, + deleted, + completed, + ] + - in: query + name: sort + schema: + type: string + enum: [added, modified] + default: added + - in: query + name: sortDirection + schema: + type: string + enum: [asc, desc] + nullable: true + default: desc + - in: query + name: requestedBy + schema: + type: number + nullable: true + example: 1 + - in: query + name: mediaType + schema: + type: string + enum: [movie, tv, all] + nullable: true + default: all + responses: + '200': + description: Requests returned + content: + application/json: + schema: + type: object + properties: + pageInfo: + $ref: '#/components/schemas/PageInfo' + results: + type: array + items: + $ref: '#/components/schemas/MediaRequest' + post: + summary: Create new request + description: | + Creates a new request with the provided media ID and type. The `REQUEST` permission is required. + + If the user has the `ADMIN` or `AUTO_APPROVE` permissions, their request will be auomatically approved. + tags: + - request + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + mediaType: + type: string + enum: [movie, tv] + example: movie + mediaId: + type: number + example: 123 + tvdbId: + type: number + example: 123 + seasons: + oneOf: + - type: array + items: + type: number + minimum: 0 + - type: string + enum: [all] + is4k: + type: boolean + example: false + serverId: + type: number + profileId: + type: number + rootFolder: + type: string + languageProfileId: + type: number + userId: + type: number + nullable: true + required: + - mediaType + - mediaId + responses: + '201': + description: Succesfully created the request + content: + application/json: + schema: + $ref: '#/components/schemas/MediaRequest' + /request/count: + get: + summary: Gets request counts + description: | + Returns the number of pending and approved requests. + tags: + - request + responses: + '200': + description: Request counts returned + content: + application/json: + schema: + type: object + properties: + total: + type: number + movie: + type: number + tv: + type: number + pending: + type: number + approved: + type: number + declined: + type: number + processing: + type: number + available: + type: number + /request/{requestId}: + get: + summary: Get MediaRequest + description: Returns a specific MediaRequest in a JSON object. + tags: + - request + parameters: + - in: path + name: requestId + description: Request ID + required: true + example: '1' + schema: + type: string + responses: + '200': + description: Succesfully returns request + content: + application/json: + schema: + $ref: '#/components/schemas/MediaRequest' + put: + summary: Update MediaRequest + description: Updates a specific media request and returns the request in a JSON object. Requires the `MANAGE_REQUESTS` permission. + tags: + - request + parameters: + - in: path + name: requestId + description: Request ID + required: true + example: '1' + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + mediaType: + type: string + enum: [movie, tv] + seasons: + type: array + items: + type: number + minimum: 0 + is4k: + type: boolean + example: false + serverId: + type: number + profileId: + type: number + rootFolder: + type: string + languageProfileId: + type: number + userId: + type: number + nullable: true + required: + - mediaType + responses: + '200': + description: Succesfully updated request + content: + application/json: + schema: + $ref: '#/components/schemas/MediaRequest' + delete: + summary: Delete request + description: Removes a request. If the user has the `MANAGE_REQUESTS` permission, any request can be removed. Otherwise, only pending requests can be removed. + tags: + - request + parameters: + - in: path + name: requestId + description: Request ID + required: true + example: '1' + schema: + type: string + responses: + '204': + description: Succesfully removed request + /request/{requestId}/retry: + post: + summary: Retry failed request + description: | + Retries a request by resending requests to Sonarr or Radarr. + + Requires the `MANAGE_REQUESTS` permission or `ADMIN`. + tags: + - request + parameters: + - in: path + name: requestId + description: Request ID + required: true + schema: + type: string + example: '1' + responses: + '200': + description: Retry triggered + content: + application/json: + schema: + $ref: '#/components/schemas/MediaRequest' + /request/{requestId}/{status}: + post: + summary: Update a request's status + description: | + Updates a request's status to approved or declined. Also returns the request in a JSON object. + + Requires the `MANAGE_REQUESTS` permission or `ADMIN`. + tags: + - request + parameters: + - in: path + name: requestId + description: Request ID + required: true + schema: + type: string + example: '1' + - in: path + name: status + description: New status + required: true + schema: + type: string + enum: [approve, decline] + responses: + '200': + description: Request status changed + content: + application/json: + schema: + $ref: '#/components/schemas/MediaRequest' + /movie/{movieId}: + get: + summary: Get movie details + description: Returns full movie details in a JSON object. + tags: + - movies + parameters: + - in: path + name: movieId + required: true + schema: + type: number + example: 337401 + - in: query + name: language + schema: + type: string + example: en + responses: + '200': + description: Movie details + content: + application/json: + schema: + $ref: '#/components/schemas/MovieDetails' + /movie/{movieId}/recommendations: + get: + summary: Get recommended movies + description: Returns list of recommended movies based on provided movie ID in a JSON object. + tags: + - movies + parameters: + - in: path + name: movieId + required: true + schema: + type: number + example: 337401 + - in: query + name: page + schema: + type: number + example: 1 + default: 1 + - in: query + name: language + schema: + type: string + example: en + responses: + '200': + description: List of movies + content: + application/json: + schema: + type: object + properties: + page: + type: number + example: 1 + totalPages: + type: number + example: 20 + totalResults: + type: number + example: 200 + results: + type: array + items: + $ref: '#/components/schemas/MovieResult' + /movie/{movieId}/similar: + get: + summary: Get similar movies + description: Returns list of similar movies based on the provided movieId in a JSON object. + tags: + - movies + parameters: + - in: path + name: movieId + required: true + schema: + type: number + example: 337401 + - in: query + name: page + schema: + type: number + example: 1 + default: 1 + - in: query + name: language + schema: + type: string + example: en + responses: + '200': + description: List of movies + content: + application/json: + schema: + type: object + properties: + page: + type: number + example: 1 + totalPages: + type: number + example: 20 + totalResults: + type: number + example: 200 + results: + type: array + items: + $ref: '#/components/schemas/MovieResult' + /movie/{movieId}/ratings: + get: + summary: Get movie ratings + description: Returns ratings based on the provided movieId in a JSON object. + tags: + - movies + parameters: + - in: path + name: movieId + required: true + schema: + type: number + example: 337401 + responses: + '200': + description: Ratings returned + content: + application/json: + schema: + type: object + properties: + title: + type: string + example: Mulan + year: + type: number + example: 2020 + url: + type: string + example: 'http://www.rottentomatoes.com/m/mulan_2020/' + criticsScore: + type: number + example: 85 + criticsRating: + type: string + enum: ['Rotten', 'Fresh', 'Certified Fresh'] + audienceScore: + type: number + example: 65 + audienceRating: + type: string + enum: ['Spilled', 'Upright'] + /movie/{movieId}/ratingscombined: + get: + summary: Get RT and IMDB movie ratings combined + description: Returns ratings from RottenTomatoes and IMDB based on the provided movieId in a JSON object. + tags: + - movies + parameters: + - in: path + name: movieId + required: true + schema: + type: number + example: 337401 + responses: + '200': + description: Ratings returned + content: + application/json: + schema: + type: object + properties: + rt: + type: object + properties: + title: + type: string + example: Mulan + year: + type: number + example: 2020 + url: + type: string + example: 'http://www.rottentomatoes.com/m/mulan_2020/' + criticsScore: + type: number + example: 85 + criticsRating: + type: string + enum: ['Rotten', 'Fresh', 'Certified Fresh'] + audienceScore: + type: number + example: 65 + audienceRating: + type: string + enum: ['Spilled', 'Upright'] + imdb: + type: object + properties: + title: + type: string + example: I am Legend + url: + type: string + example: 'https://www.imdb.com/title/tt0480249' + criticsScore: + type: number + example: 6.5 + /tv/{tvId}: + get: + summary: Get TV details + description: Returns full TV details in a JSON object. + tags: + - tv + parameters: + - in: path + name: tvId + required: true + schema: + type: number + example: 76479 + - in: query + name: language + schema: + type: string + example: en + responses: + '200': + description: TV details + content: + application/json: + schema: + $ref: '#/components/schemas/TvDetails' + /tv/{tvId}/season/{seasonId}: + get: + summary: Get season details and episode list + description: Returns season details with a list of episodes in a JSON object. + tags: + - tv + parameters: + - in: path + name: tvId + required: true + schema: + type: number + example: 76479 + - in: path + name: seasonId + required: true + schema: + type: number + example: 1 + - in: query + name: language + schema: + type: string + example: en + responses: + '200': + description: TV details + content: + application/json: + schema: + $ref: '#/components/schemas/Season' + /tv/{tvId}/recommendations: + get: + summary: Get recommended TV series + description: Returns list of recommended TV series based on the provided tvId in a JSON object. + tags: + - tv + parameters: + - in: path + name: tvId + required: true + schema: + type: number + example: 76479 + - in: query + name: page + schema: + type: number + example: 1 + default: 1 + - in: query + name: language + schema: + type: string + example: en + responses: + '200': + description: List of TV series + content: + application/json: + schema: + type: object + properties: + page: + type: number + example: 1 + totalPages: + type: number + example: 20 + totalResults: + type: number + example: 200 + results: + type: array + items: + $ref: '#/components/schemas/TvResult' + /tv/{tvId}/similar: + get: + summary: Get similar TV series + description: Returns list of similar TV series based on the provided tvId in a JSON object. + tags: + - tv + parameters: + - in: path + name: tvId + required: true + schema: + type: number + example: 76479 + - in: query + name: page + schema: + type: number + example: 1 + default: 1 + - in: query + name: language + schema: + type: string + example: en + responses: + '200': + description: List of TV series + content: + application/json: + schema: + type: object + properties: + page: + type: number + example: 1 + totalPages: + type: number + example: 20 + totalResults: + type: number + example: 200 + results: + type: array + items: + $ref: '#/components/schemas/TvResult' + /tv/{tvId}/ratings: + get: + summary: Get TV ratings + description: Returns ratings based on provided tvId in a JSON object. + tags: + - tv + parameters: + - in: path + name: tvId + required: true + schema: + type: number + example: 76479 + responses: + '200': + description: Ratings returned + content: + application/json: + schema: + type: object + properties: + title: + type: string + example: The Boys + year: + type: number + example: 2019 + url: + type: string + example: 'http://www.rottentomatoes.com/m/mulan_2020/' + criticsScore: + type: number + example: 85 + criticsRating: + type: string + enum: ['Rotten', 'Fresh'] + /person/{personId}: + get: + summary: Get person details + description: Returns person details based on provided personId in a JSON object. + tags: + - person + parameters: + - in: path + name: personId + required: true + schema: + type: number + example: 287 + - in: query + name: language + schema: + type: string + example: en + responses: + '200': + description: Returned person + content: + application/json: + schema: + $ref: '#/components/schemas/PersonDetails' + /person/{personId}/combined_credits: + get: + summary: Get combined credits + description: Returns the person's combined credits based on the provided personId in a JSON object. + tags: + - person + parameters: + - in: path + name: personId + required: true + schema: + type: number + example: 287 + - in: query + name: language + schema: + type: string + example: en + responses: + '200': + description: Returned combined credits + content: + application/json: + schema: + type: object + properties: + cast: + type: array + items: + $ref: '#/components/schemas/CreditCast' + crew: + type: array + items: + $ref: '#/components/schemas/CreditCrew' + id: + type: number + /media: + get: + summary: Get media + description: Returns all media (can be filtered and limited) in a JSON object. + tags: + - media + parameters: + - in: query + name: take + schema: + type: number + nullable: true + example: 20 + - in: query + name: skip + schema: + type: number + nullable: true + example: 0 + - in: query + name: filter + schema: + type: string + nullable: true + enum: + [ + all, + available, + partial, + allavailable, + processing, + pending, + deleted, + ] + - in: query + name: sort + schema: + type: string + enum: [added, modified, mediaAdded] + default: added + responses: + '200': + description: Returned media + content: + application/json: + schema: + type: object + properties: + pageInfo: + $ref: '#/components/schemas/PageInfo' + results: + type: array + items: + $ref: '#/components/schemas/MediaInfo' + /media/{mediaId}: + delete: + summary: Delete media item + description: Removes a media item. The `MANAGE_REQUESTS` permission is required to perform this action. + tags: + - media + parameters: + - in: path + name: mediaId + description: Media ID + required: true + example: '1' + schema: + type: string + responses: + '204': + description: Succesfully removed media item + /media/{mediaId}/file: + delete: + summary: Delete media file + description: Removes a media file from radarr/sonarr. The `ADMIN` permission is required to perform this action. + tags: + - media + parameters: + - in: path + name: mediaId + description: Media ID + required: true + example: '1' + schema: + type: string + responses: + '204': + description: Succesfully removed media item + /media/{mediaId}/{status}: + post: + summary: Update media status + description: Updates a media item's status and returns the media in JSON format + tags: + - media + parameters: + - in: path + name: mediaId + description: Media ID + required: true + example: '1' + schema: + type: string + - in: path + name: status + description: New status + required: true + example: available + schema: + type: string + enum: [available, partial, processing, pending, unknown, deleted] + requestBody: + content: + application/json: + schema: + type: object + properties: + is4k: + type: boolean + example: false + responses: + '200': + description: Returned media + content: + application/json: + schema: + $ref: '#/components/schemas/MediaInfo' + /media/{mediaId}/watch_data: + get: + summary: Get watch data + description: | + Returns play count, play duration, and users who have watched the media. + + Requires the `ADMIN` permission. + tags: + - media + parameters: + - in: path + name: mediaId + description: Media ID + required: true + example: '1' + schema: + type: string + responses: + '200': + description: Users + content: + application/json: + schema: + type: object + properties: + data: + type: object + properties: + playCount7Days: + type: number + playCount30Days: + type: number + playCount: + type: number + users: + type: array + items: + $ref: '#/components/schemas/User' + data4k: + type: object + properties: + playCount7Days: + type: number + playCount30Days: + type: number + playCount: + type: number + users: + type: array + items: + $ref: '#/components/schemas/User' + /collection/{collectionId}: + get: + summary: Get collection details + description: Returns full collection details in a JSON object. + tags: + - collection + parameters: + - in: path + name: collectionId + required: true + schema: + type: number + example: 537982 + - in: query + name: language + schema: + type: string + example: en + responses: + '200': + description: Collection details + content: + application/json: + schema: + $ref: '#/components/schemas/Collection' + /service/radarr: + get: + summary: Get non-sensitive Radarr server list + description: Returns a list of Radarr server IDs and names in a JSON object. + tags: + - service + responses: + '200': + description: Request successful + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/RadarrSettings' + /service/radarr/{radarrId}: + get: + summary: Get Radarr server quality profiles and root folders + description: Returns a Radarr server's quality profile and root folder details in a JSON object. + tags: + - service + parameters: + - in: path + name: radarrId + required: true + schema: + type: number + example: 0 + responses: + '200': + description: Request successful + content: + application/json: + schema: + type: object + properties: + server: + $ref: '#/components/schemas/RadarrSettings' + profiles: + $ref: '#/components/schemas/ServiceProfile' + /service/sonarr: + get: + summary: Get non-sensitive Sonarr server list + description: Returns a list of Sonarr server IDs and names in a JSON object. + tags: + - service + responses: + '200': + description: Request successful + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/SonarrSettings' + /service/sonarr/{sonarrId}: + get: + summary: Get Sonarr server quality profiles and root folders + description: Returns a Sonarr server's quality profile and root folder details in a JSON object. + tags: + - service + parameters: + - in: path + name: sonarrId + required: true + schema: + type: number + example: 0 + responses: + '200': + description: Request successful + content: + application/json: + schema: + type: object + properties: + server: + $ref: '#/components/schemas/SonarrSettings' + profiles: + $ref: '#/components/schemas/ServiceProfile' + /service/sonarr/lookup/{tmdbId}: + get: + summary: Get series from Sonarr + description: Returns a list of series returned by searching for the name in Sonarr. + tags: + - service + parameters: + - in: path + name: tmdbId + required: true + schema: + type: number + example: 0 + responses: + '200': + description: Request successful + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/SonarrSeries' + /regions: + get: + summary: Regions supported by TMDB + description: Returns a list of regions in a JSON object. + tags: + - tmdb + responses: + '200': + description: Results + content: + application/json: + schema: + type: array + items: + type: object + properties: + iso_3166_1: + type: string + example: US + english_name: + type: string + example: United States of America + /languages: + get: + summary: Languages supported by TMDB + description: Returns a list of languages in a JSON object. + tags: + - tmdb + responses: + '200': + description: Results + content: + application/json: + schema: + type: array + items: + type: object + properties: + iso_639_1: + type: string + example: en + english_name: + type: string + example: English + name: + type: string + example: English + /studio/{studioId}: + get: + summary: Get movie studio details + description: Returns movie studio details in a JSON object. + tags: + - tmdb + parameters: + - in: path + name: studioId + required: true + schema: + type: number + example: 2 + responses: + '200': + description: Movie studio details + content: + application/json: + schema: + $ref: '#/components/schemas/ProductionCompany' + /network/{networkId}: + get: + summary: Get TV network details + description: Returns TV network details in a JSON object. + tags: + - tmdb + parameters: + - in: path + name: networkId + required: true + schema: + type: number + example: 1 + responses: + '200': + description: TV network details + content: + application/json: + schema: + $ref: '#/components/schemas/ProductionCompany' + /genres/movie: + get: + summary: Get list of official TMDB movie genres + description: Returns a list of genres in a JSON array. + tags: + - tmdb + parameters: + - in: query + name: language + schema: + type: string + example: en + responses: + '200': + description: Results + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: number + example: 10751 + name: + type: string + example: Family + /genres/tv: + get: + summary: Get list of official TMDB movie genres + description: Returns a list of genres in a JSON array. + tags: + - tmdb + parameters: + - in: query + name: language + schema: + type: string + example: en + responses: + '200': + description: Results + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: number + example: 18 + name: + type: string + example: Drama + /backdrops: + get: + summary: Get backdrops of trending items + description: Returns a list of backdrop image paths in a JSON array. + security: [] + tags: + - tmdb + responses: + '200': + description: Results + content: + application/json: + schema: + type: array + items: + type: string + /issue: + get: + summary: Get all issues + description: | + Returns a list of issues in JSON format. + tags: + - issue + parameters: + - in: query + name: take + schema: + type: number + nullable: true + example: 20 + - in: query + name: skip + schema: + type: number + nullable: true + example: 0 + - in: query + name: sort + schema: + type: string + enum: [added, modified] + default: added + - in: query + name: filter + schema: + type: string + enum: [all, open, resolved] + default: open + - in: query + name: requestedBy + schema: + type: number + nullable: true + example: 1 + responses: + '200': + description: Issues returned + content: + application/json: + schema: + type: object + properties: + pageInfo: + $ref: '#/components/schemas/PageInfo' + results: + type: array + items: + $ref: '#/components/schemas/Issue' + post: + summary: Create new issue + description: | + Creates a new issue + tags: + - issue + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + issueType: + type: number + message: + type: string + mediaId: + type: number + responses: + '201': + description: Succesfully created the issue + content: + application/json: + schema: + $ref: '#/components/schemas/Issue' + + /issue/count: + get: + summary: Gets issue counts + description: | + Returns the number of open and closed issues, as well as the number of issues of each type. + tags: + - issue + responses: + '200': + description: Issue counts returned + content: + application/json: + schema: + type: object + properties: + total: + type: number + video: + type: number + audio: + type: number + subtitles: + type: number + others: + type: number + open: + type: number + closed: + type: number + /issue/{issueId}: + get: + summary: Get issue + description: | + Returns a single issue in JSON format. + tags: + - issue + parameters: + - in: path + name: issueId + required: true + schema: + type: number + example: 1 + responses: + '200': + description: Issues returned + content: + application/json: + schema: + $ref: '#/components/schemas/Issue' + delete: + summary: Delete issue + description: Removes an issue. If the user has the `MANAGE_ISSUES` permission, any issue can be removed. Otherwise, only a users own issues can be removed. + tags: + - issue + parameters: + - in: path + name: issueId + description: Issue ID + required: true + example: '1' + schema: + type: string + responses: + '204': + description: Succesfully removed issue + /issue/{issueId}/comment: + post: + summary: Create a comment + description: | + Creates a comment and returns associated issue in JSON format. + tags: + - issue + parameters: + - in: path + name: issueId + required: true + schema: + type: number + example: 1 + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + message: + type: string + required: + - message + responses: + '200': + description: Issue returned with new comment + content: + application/json: + schema: + $ref: '#/components/schemas/Issue' + /issueComment/{commentId}: + get: + summary: Get issue comment + description: | + Returns a single issue comment in JSON format. + tags: + - issue + parameters: + - in: path + name: commentId + required: true + schema: + type: string + example: 1 + responses: + '200': + description: Comment returned + content: + application/json: + schema: + $ref: '#/components/schemas/IssueComment' + put: + summary: Update issue comment + description: | + Updates and returns a single issue comment in JSON format. + tags: + - issue + parameters: + - in: path + name: commentId + required: true + schema: + type: string + example: 1 + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + message: + type: string + responses: + '200': + description: Comment updated + content: + application/json: + schema: + $ref: '#/components/schemas/IssueComment' + delete: + summary: Delete issue comment + description: | + Deletes an issue comment. Only users with `MANAGE_ISSUES` or the user who created the comment can perform this action. + tags: + - issue + parameters: + - in: path + name: commentId + description: Issue Comment ID + required: true + example: '1' + schema: + type: string + responses: + '204': + description: Succesfully removed issue comment + /issue/{issueId}/{status}: + post: + summary: Update an issue's status + description: | + Updates an issue's status to approved or declined. Also returns the issue in a JSON object. + + Requires the `MANAGE_ISSUES` permission or `ADMIN`. + tags: + - issue + parameters: + - in: path + name: issueId + description: Issue ID + required: true + schema: + type: string + example: '1' + - in: path + name: status + description: New status + required: true + schema: + type: string + enum: [open, resolved] + responses: + '200': + description: Issue status changed + content: + application/json: + schema: + $ref: '#/components/schemas/Issue' + /keyword/{keywordId}: + get: + summary: Get keyword + description: | + Returns a single keyword in JSON format. + tags: + - other + parameters: + - in: path + name: keywordId + required: true + schema: + type: number + example: 1 + responses: + '200': + description: Keyword returned + content: + application/json: + schema: + $ref: '#/components/schemas/Keyword' + /watchproviders/regions: + get: + summary: Get watch provider regions + description: | + Returns a list of all available watch provider regions. + tags: + - other + responses: + '200': + description: Watch provider regions returned + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/WatchProviderRegion' + /watchproviders/movies: + get: + summary: Get watch provider movies + description: | + Returns a list of all available watch providers for movies. + tags: + - other + parameters: + - in: query + name: watchRegion + required: true + schema: + type: string + example: US + responses: + '200': + description: Watch providers for movies returned + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/WatchProviderDetails' + /watchproviders/tv: + get: + summary: Get watch provider series + description: | + Returns a list of all available watch providers for series. + tags: + - other + parameters: + - in: query + name: watchRegion + required: true + schema: + type: string + example: US + responses: + '200': + description: Watch providers for series returned + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/WatchProviderDetails' + /certifications/movie: + get: + summary: Get movie certifications + description: Returns list of movie certifications from TMDB. + tags: + - other + security: + - cookieAuth: [] + - apiKey: [] + responses: + '200': + description: Movie certifications returned + content: + application/json: + schema: + $ref: '#/components/schemas/CertificationResponse' + '500': + description: Unable to retrieve movie certifications + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + example: Unable to retrieve movie certifications. + /certifications/tv: + get: + summary: Get TV certifications + description: Returns list of TV show certifications from TMDB. + tags: + - other + security: + - cookieAuth: [] + - apiKey: [] + responses: + '200': + description: TV certifications returned + content: + application/json: + schema: + $ref: '#/components/schemas/CertificationResponse' + '500': + description: Unable to retrieve TV certifications + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + example: Unable to retrieve TV certifications. + /overrideRule: + get: + summary: Get override rules + description: Returns a list of all override rules with their conditions and settings + tags: + - overriderule + responses: + '200': + description: Override rules returned + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/OverrideRule' + post: + summary: Create override rule + description: Creates a new Override Rule from the request body. + tags: + - overriderule + responses: + '200': + description: 'Values were successfully created' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/OverrideRule' + /overrideRule/{ruleId}: + put: + summary: Update override rule + description: Updates an Override Rule from the request body. + tags: + - overriderule + parameters: + - in: path + name: ruleId + required: true + schema: + type: number + responses: + '200': + description: 'Values were successfully updated' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/OverrideRule' + delete: + summary: Delete override rule by ID + description: Deletes the override rule with the provided ruleId. + tags: + - overriderule + parameters: + - in: path + name: ruleId + required: true + schema: + type: number + responses: + '200': + description: Override rule successfully deleted + content: + application/json: + schema: + $ref: '#/components/schemas/OverrideRule' +security: + - cookieAuth: [] + - apiKey: [] diff --git a/jellyseerr_api.py b/jellyseerr_api.py new file mode 100644 index 0000000..9f7910b --- /dev/null +++ b/jellyseerr_api.py @@ -0,0 +1,322 @@ +import aiohttp +import asyncio +import config +import urllib.parse +import json +import logging +from datetime import datetime, timedelta +from typing import Dict, List, Optional, Any, Union + +logger = logging.getLogger('jellyseerr_api') + +class JellyseerrAPI: + def __init__(self, base_url: str, email: str = None, password: str = None): + self.base_url = base_url.rstrip('/') + self.email = email + self.password = password + self.headers = { + 'Content-Type': 'application/json' + } + self.session = None + self.cookie_jar = None + self.auth_cookie = None + self.last_login = None + + async def __aenter__(self): + if not self.cookie_jar: + self.cookie_jar = aiohttp.CookieJar() + self.session = aiohttp.ClientSession(cookie_jar=self.cookie_jar) + await self.login() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + if self.session: + await self.session.close() + + async def login(self) -> bool: + """Log in to Jellyseerr using local authentication""" + # Skip login if we have a valid cookie and it's not expired + cookie_expiry_days = config.AUTH_COOKIE_EXPIRY + if (self.auth_cookie and self.last_login and + datetime.now() < self.last_login + timedelta(days=cookie_expiry_days)): + logger.debug("Using existing auth cookie") + return True + + if not self.session: + if not self.cookie_jar: + self.cookie_jar = aiohttp.CookieJar() + self.session = aiohttp.ClientSession(cookie_jar=self.cookie_jar) + + try: + login_data = { + 'email': self.email, + 'password': self.password + } + + logger.info(f"Logging in to Jellyseerr with email: {self.email}") + logger.debug(f"Login URL: {self.base_url}/api/v1/auth/local") + + # Print request info for debugging + logger.debug(f"Request headers: {self.session.headers}") + logger.debug(f"Login payload (without password): {{'email': '{self.email}'}}") + + async with self.session.post( + f"{self.base_url}/api/v1/auth/local", + json=login_data + ) as response: + response_text = await response.text() + logger.debug(f"Login response status: {response.status}") + logger.debug(f"Login response headers: {response.headers}") + logger.debug(f"Login response body: {response_text}") + + if response.status == 200: + # Login successful, cookies are automatically stored in the cookie jar + self.last_login = datetime.now() + logger.info("Successfully logged in to Jellyseerr") + return True + else: + # Try to parse response as JSON, but handle case where it's not JSON + try: + error_data = json.loads(response_text) + error_message = error_data.get('message', 'Unknown error') + except json.JSONDecodeError: + error_message = f"Non-JSON response (Status {response.status}): {response_text}" + + logger.error(f"Login failed: {error_message}") + + if response.status == 401: + raise Exception(f"Authentication failed: Invalid email or password") + elif response.status == 403: + raise Exception(f"Authentication failed: Access denied. Account may be disabled or lacks permissions") + elif response.status == 500: + raise Exception(f"Authentication failed: Server error. Check Jellyseerr logs") + else: + raise Exception(f"Authentication failed ({response.status}): {error_message}") + + except aiohttp.ClientConnectorError as e: + logger.error(f"Connection error: {str(e)}") + raise Exception(f"Failed to connect to Jellyseerr at {self.base_url}: {str(e)}") + except aiohttp.ClientError as e: + logger.error(f"Client error: {str(e)}") + raise Exception(f"HTTP client error: {str(e)}") + except Exception as e: + logger.error(f"Login error: {str(e)}", exc_info=True) + raise Exception(f"Failed to authenticate with Jellyseerr: {str(e)}") + + async def _request(self, method: str, endpoint: str, params: Dict = None, data: Dict = None) -> Any: + # Ensure we're logged in + if not self.last_login: + try: + await self.login() + except Exception as e: + logger.warning(f"Authentication failed, attempting request without authentication: {str(e)}") + # Continue anyway - some endpoints might work without auth + + # Create session if it doesn't exist + if self.session is None: + if not self.cookie_jar: + self.cookie_jar = aiohttp.CookieJar() + self.session = aiohttp.ClientSession(cookie_jar=self.cookie_jar) + + # Debug info + logger.debug(f"Making request: {method} {self.base_url}/api/v1{endpoint}") + if params: + logger.debug(f"Request params: {params}") + if data: + logger.debug(f"Request data: {data}") + + url = f"{self.base_url}/api/v1{endpoint}" + + # URL encode parameters + if params: + # Create a new dict with URL-encoded values + encoded_params = {} + for key, value in params.items(): + if isinstance(value, str): + encoded_params[key] = urllib.parse.quote(value) + else: + encoded_params[key] = value + params = encoded_params + + try: + # Attempt the request + async with self.session.request(method, url, headers=self.headers, params=params, json=data) as response: + if response.status == 204: # No content + return None + + try: + response_data = await response.json() + + # If unauthorized, try to login again and retry + if response.status == 401: + logger.warning("Received unauthorized response, attempting to re-login") + try: + await self.login() + # Retry the request after re-login + async with self.session.request(method, url, headers=self.headers, params=params, json=data) as retry_response: + logger.debug(f"Retry response status: {retry_response.status}") + + if retry_response.status == 204: + return None + + try: + retry_data = await retry_response.json() + if not retry_response.ok: + error_message = retry_data.get('message', 'Unknown error') + raise Exception(f"API Error ({retry_response.status}): {error_message}") + + return retry_data + except aiohttp.ContentTypeError: + # Handle non-JSON responses + retry_text = await retry_response.text() + logger.error(f"Non-JSON response on retry: {retry_text}") + raise Exception(f"API returned non-JSON response: {retry_text[:100]}...") + except Exception as login_err: + logger.error(f"Re-login failed: {str(login_err)}") + raise Exception(f"Authentication error: {str(login_err)}") + + if not response.ok: + error_message = response_data.get('message', 'Unknown error') + raise Exception(f"API Error ({response.status}): {error_message}") + + return response_data + except aiohttp.ClientResponseError: + raise Exception(f"API Error ({response.status}): Failed to parse response") + except aiohttp.ClientError as e: + logger.error(f"Connection error: {str(e)}") + raise Exception(f"Connection error: {str(e)}") + + # Search endpoints + async def search(self, query: str, page: int = 1, language: str = 'en') -> Dict: + """Search for movies, TV shows, or people""" + params = { + 'query': query, + 'page': page, + 'language': language + } + return await self._request('GET', '/search', params=params) + + # Media information endpoints + async def get_movie_details(self, movie_id: int, language: str = 'en') -> Dict: + """Get detailed information about a movie""" + params = {'language': language} + return await self._request('GET', f'/movie/{movie_id}', params=params) + + async def get_tv_details(self, tv_id: int, language: str = 'en') -> Dict: + """Get detailed information about a TV show""" + params = {'language': language} + return await self._request('GET', f'/tv/{tv_id}', params=params) + + async def get_season_details(self, tv_id: int, season_id: int, language: str = 'en') -> Dict: + """Get detailed information about a TV season""" + params = {'language': language} + return await self._request('GET', f'/tv/{tv_id}/season/{season_id}', params=params) + + # Recommendation endpoints + async def get_movie_recommendations(self, movie_id: int, page: int = 1, language: str = 'en') -> Dict: + """Get movie recommendations based on a movie""" + params = { + 'page': page, + 'language': language + } + return await self._request('GET', f'/movie/{movie_id}/recommendations', params=params) + + async def get_tv_recommendations(self, tv_id: int, page: int = 1, language: str = 'en') -> Dict: + """Get TV show recommendations based on a TV show""" + params = { + 'page': page, + 'language': language + } + return await self._request('GET', f'/tv/{tv_id}/recommendations', params=params) + + # Request endpoints + async def get_requests(self, filter_status: str = 'all', page: int = 1, page_size: int = 10) -> Dict: + """Get all requests with pagination""" + params = { + 'filter': filter_status, + 'take': page_size, + 'skip': (page - 1) * page_size + } + return await self._request('GET', '/request', params=params) + + async def create_request(self, media_type: str, media_id: int, + seasons: Union[List[int], str] = None, + is_4k: bool = False) -> Dict: + """Create a new media request""" + data = { + 'mediaType': media_type, + 'mediaId': media_id, + 'is4k': is_4k + } + + if media_type == 'tv' and seasons: + data['seasons'] = seasons + + return await self._request('POST', '/request', data=data) + + async def get_request(self, request_id: int) -> Dict: + """Get details of a specific request""" + return await self._request('GET', f'/request/{request_id}') + + # Methods for approving and declining requests have been removed + # These actions should be performed in the Jellyseerr web interface + + # Discover endpoints + async def discover_movies(self, page: int = 1, language: str = 'en', + genre: Optional[str] = None, sort_by: str = 'popularity.desc') -> Dict: + """Discover movies with various filters""" + params = { + 'page': page, + 'language': language, + 'sortBy': sort_by + } + + if genre: + params['genre'] = genre + + return await self._request('GET', '/discover/movies', params=params) + + async def discover_tv(self, page: int = 1, language: str = 'en', + genre: Optional[str] = None, sort_by: str = 'popularity.desc') -> Dict: + """Discover TV shows with various filters""" + params = { + 'page': page, + 'language': language, + 'sortBy': sort_by + } + + if genre: + params['genre'] = genre + + return await self._request('GET', '/discover/tv', params=params) + + async def discover_trending(self, page: int = 1, language: str = 'en') -> Dict: + """Get trending movies and TV shows""" + params = { + 'page': page, + 'language': language + } + return await self._request('GET', '/discover/trending', params=params) + + # Genre endpoints + async def get_movie_genres(self, language: str = 'en') -> List: + """Get a list of movie genres""" + params = {'language': language} + return await self._request('GET', '/genres/movie', params=params) + + async def get_tv_genres(self, language: str = 'en') -> List: + """Get a list of TV show genres""" + params = {'language': language} + return await self._request('GET', '/genres/tv', params=params) + + async def get_media(self, filter_status: str = 'all', page: int = 1, page_size: int = 10) -> Dict: + """Get all media with pagination""" + params = { + 'filter': filter_status, + 'take': page_size, + 'skip': (page - 1) * page_size + } + return await self._request('GET', '/media', params=params) + +# Create an instance for import (but don't initialize cookie_jar yet) +jellyseerr_api = JellyseerrAPI(config.JELLYSEERR_URL, config.JELLYSEERR_EMAIL, config.JELLYSEERR_PASSWORD) \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..9af8f9f --- /dev/null +++ b/main.py @@ -0,0 +1,209 @@ +import os +import logging +from logging.handlers import RotatingFileHandler +import discord +from discord.ext import commands +import asyncio +import config +import sys +import traceback + +# Set up logging +def setup_logging(): + log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + detailed_log_format = '%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s' + log_level = logging.INFO + + # Create logs directory if it doesn't exist + os.makedirs('logs', exist_ok=True) + + # Configure root logger + root_logger = logging.getLogger() + root_logger.setLevel(log_level) + + # Console handler + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setFormatter(logging.Formatter(debug_format if config.DEBUG_MODE else log_format)) + console_handler.setLevel(log_level) + + # File handler with rotation (5 files, 5MB each) + file_handler = RotatingFileHandler( + filename='logs/bot.log', + maxBytes=5 * 1024 * 1024, # 5MB + backupCount=5, + encoding='utf-8', + mode='a' # Append mode instead of overwrite + ) + file_handler.setFormatter(logging.Formatter(detailed_log_format)) + file_handler.setLevel(logging.DEBUG if config.DEBUG_MODE else log_level) + + # Error file handler with rotation (5 files, 5MB each) + error_file_handler = RotatingFileHandler( + filename='logs/error.log', + maxBytes=5 * 1024 * 1024, # 5MB + backupCount=5, + encoding='utf-8', + mode='a' # Append mode instead of overwrite + ) + error_file_handler.setFormatter(logging.Formatter(detailed_log_format)) + error_file_handler.setLevel(logging.ERROR) + + # Add handlers + root_logger.addHandler(console_handler) + root_logger.addHandler(file_handler) + root_logger.addHandler(error_file_handler) + + return logging.getLogger('bot') + +logger = setup_logging() + +# Initialize the bot with intents +intents = discord.Intents.default() +intents.message_content = True +intents.members = True + +# Create the bot instance +bot = commands.Bot( + command_prefix=config.BOT_PREFIX, + intents=intents, + help_command=None # Disable default help command +) + +# Create API client variable (will be initialized later) +jellyseerr_api = None + +# Load all cogs +async def load_extensions(): + # Import here to avoid circular imports + from commands import MediaCommands, RequestCommands + from utility_commands import UtilityCommands + from jellyseerr_api import JellyseerrAPI + + # Initialize the API client + global jellyseerr_api + jellyseerr_api = JellyseerrAPI(config.JELLYSEERR_URL, config.JELLYSEERR_EMAIL, config.JELLYSEERR_PASSWORD) + + # Make API client available to command modules + import sys + sys.modules['jellyseerr_api'].jellyseerr_api = jellyseerr_api + + await bot.add_cog(MediaCommands(bot)) + await bot.add_cog(RequestCommands(bot)) + await bot.add_cog(UtilityCommands(bot)) + logger.info("Loaded all extensions") + +@bot.event +async def on_ready(): + logger.info(f'Logged in as {bot.user} (ID: {bot.user.id})') + logger.info(f'Connected to {len(bot.guilds)} guilds') + for guild in bot.guilds: + logger.info(f' - {guild.name} (ID: {guild.id})') + logger.info(f'Using prefix: {config.BOT_PREFIX}') + logger.info(f'Jellyseerr URL: {config.JELLYSEERR_URL}') + + # Test Jellyseerr authentication + if config.JELLYSEERR_LOCAL_LOGIN: + try: + logger.info(f'Authenticating with Jellyseerr using email: {config.JELLYSEERR_EMAIL}') + if config.DEBUG_MODE: + logger.debug(f'Jellyseerr URL: {config.JELLYSEERR_URL}') + logger.debug(f'Using local login with email: {config.JELLYSEERR_EMAIL}') + + await jellyseerr_api.login() + logger.info('Successfully authenticated with Jellyseerr') + except Exception as e: + logger.error(f'Failed to authenticate with Jellyseerr: {str(e)}', exc_info=True) + logger.warning('The bot will attempt to reconnect when commands are used') + + if config.DEBUG_MODE: + logger.debug(f'Authentication error details: {traceback.format_exc()}') + logger.debug('Check that:') + logger.debug('1. Your Jellyseerr URL is correct') + logger.debug('2. Local login is enabled in Jellyseerr settings') + logger.debug('3. The email and password are correct') + logger.debug('4. The user account has not been disabled') + else: + logger.info('Local login disabled, proceeding without authentication') + + # Set bot activity + activity = discord.Activity( + type=discord.ActivityType.watching, + name=f"{config.BOT_PREFIX}help | Jellyseerr" + ) + await bot.change_presence(activity=activity) + logger.info("Bot is ready and fully operational") + +@bot.event +async def on_command(ctx): + logger.info(f"Command '{ctx.command}' invoked by {ctx.author} in {ctx.guild}/{ctx.channel}") + +@bot.event +async def on_command_completion(ctx): + logger.info(f"Command '{ctx.command}' completed successfully for {ctx.author}") + +@bot.event +async def on_command_error(ctx, error): + if isinstance(error, commands.CommandNotFound): + logger.debug(f"Command not found: {ctx.message.content}") + return + elif isinstance(error, commands.MissingRequiredArgument): + logger.info(f"Missing required argument: {error.param.name} for command {ctx.command} by user {ctx.author}") + await ctx.send(f"Missing required argument: `{error.param.name}`. Type `{config.BOT_PREFIX}help {ctx.command}` for correct usage.") + elif isinstance(error, commands.BadArgument): + logger.info(f"Bad argument for command {ctx.command} by user {ctx.author}: {str(error)}") + await ctx.send(f"Invalid argument: `{str(error)}`. Please check your input and try again.") + elif isinstance(error, commands.MissingPermissions): + logger.info(f"Permission denied for {ctx.author} trying to use {ctx.command}") + await ctx.send("You don't have permission to use this command.") + elif isinstance(error, commands.CommandOnCooldown): + logger.info(f"Command on cooldown for {ctx.author} trying to use {ctx.command}") + await ctx.send(f"This command is on cooldown. Please try again in {error.retry_after:.1f} seconds.") + elif isinstance(error, commands.DisabledCommand): + logger.info(f"Disabled command {ctx.command} attempted by {ctx.author}") + await ctx.send("This command is currently disabled.") + elif isinstance(error, commands.NoPrivateMessage): + logger.info(f"Command {ctx.command} not allowed in DMs, attempted by {ctx.author}") + await ctx.send("This command cannot be used in private messages.") + else: + # Get original error if it's wrapped in CommandInvokeError + original = error.original if hasattr(error, 'original') else error + + # Create a detailed error message for logging + error_details = f"Command error in {ctx.command} invoked by {ctx.author}: {str(original)}" + logger.error(error_details, exc_info=True) + + # Create a user-friendly error message + if "400" in str(original) and "must be url encoded" in str(original).lower(): + await ctx.send("Error processing your request: Your input contains special characters that couldn't be processed. Please try again with simpler text.") + elif "API Error" in str(original): + await ctx.send(f"Jellyseerr API error: {str(original).split('API Error')[1].strip()}") + else: + await ctx.send(f"An error occurred while processing your command. Please try again later or check the syntax with `{config.BOT_PREFIX}help {ctx.command}`.") + +async def main(): + try: + async with bot: + logger.info("Starting Discord Jellyseerr Bot") + if config.JELLYSEERR_LOCAL_LOGIN: + logger.info(f"Using Jellyseerr authentication with account: {config.JELLYSEERR_EMAIL}") + else: + logger.info("Local login disabled, some functionality may be limited") + + if config.DEBUG_MODE: + logger.debug("DEBUG MODE ENABLED - verbose logging activated") + + await load_extensions() + await bot.start(config.BOT_TOKEN) + except KeyboardInterrupt: + logger.info("Bot shutting down by keyboard interrupt") + except Exception as e: + logger.critical(f"Fatal error: {str(e)}", exc_info=True) + sys.exit(1) + +if __name__ == "__main__": + try: + asyncio.run(main()) + except Exception as e: + print(f"Fatal error occurred: {str(e)}") + logging.critical(f"Fatal error in main process: {str(e)}", exc_info=True) + sys.exit(1) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a578942 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[tool.pyright] +venvPath = "." +venv = ".venv" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..67a2b70 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +discord.py>=2.0.0 +python-dotenv>=0.19.0 +aiohttp>=3.8.0 +requests>=2.26.0 +asyncio>=3.4.3 +pyyaml>=6.0 +python-dateutil>=2.8.2 \ No newline at end of file diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..2341a95 --- /dev/null +++ b/run.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# Exit on error +set -e + +# Check if virtual environment exists +if [ ! -d ".venv" ]; then + echo "Creating virtual environment..." + python3 -m venv .venv +fi + +# Activate virtual environment +source .venv/bin/activate + +# Install dependencies +echo "Installing dependencies..." +pip install -r requirements.txt + +# Run the bot +echo "Starting Discord Jellyseerr Bot..." +python main.py \ No newline at end of file diff --git a/utility_commands.py b/utility_commands.py new file mode 100644 index 0000000..3dcfac6 --- /dev/null +++ b/utility_commands.py @@ -0,0 +1,348 @@ +import discord +from discord.ext import commands +import asyncio +import aiohttp +from typing import Optional, List, Dict, Union, Any +import logging + +import config +from commands import safe_int_convert, get_api + +logger = logging.getLogger('utility_commands') + +class UtilityCommands(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.embed_color = config.EMBED_COLOR + self.tmdb_base_url = "https://api.themoviedb.org/3" + + @commands.command(name="help", aliases=["h"]) + async def custom_help(self, ctx, command: str = None): + """Show help information for all commands or a specific command""" + if command: + cmd = self.bot.get_command(command) + if cmd: + embed = discord.Embed( + title=f"Help: {config.BOT_PREFIX}{cmd.name}", + description=cmd.help or "No description available", + color=self.embed_color + ) + + # Add aliases if any + if cmd.aliases: + embed.add_field( + name="Aliases", + value=", ".join([f"`{config.BOT_PREFIX}{alias}`" for alias in cmd.aliases]), + inline=False + ) + + # Add usage + embed.add_field( + name="Usage", + value=f"`{config.BOT_PREFIX}{cmd.name} {cmd.signature}`", + inline=False + ) + + await ctx.send(embed=embed) + return + else: + await ctx.send(f"Command `{command}` not found. Try `{config.BOT_PREFIX}help` to see all commands.") + return + embed = discord.Embed( + title="Jellyseerr Discord Bot Help", + description="Here are the commands you can use:", + color=self.embed_color + ) + + # Search & Discovery Commands + embed.add_field( + name="🔍 Search & Discovery", + value=( + f"`{config.BOT_PREFIX}search ` - Search for movies and TV shows\n" + f"`{config.BOT_PREFIX}info ` - Get detailed information about any media (auto-detects type)\n" + f"`{config.BOT_PREFIX}trending` - Show trending movies and TV shows" + ), + inline=False + ) + + # Request Commands + embed.add_field( + name="📝 Requests", + value=( + f"`{config.BOT_PREFIX}request movie ` - Request a movie\n" + f"`{config.BOT_PREFIX}request tv [seasons]` - Request a TV show (all or specific seasons)\n" + f"`{config.BOT_PREFIX}requests [status] [page]` - List media requests\n" + ), + inline=False + ) + + # Other Commands + embed.add_field( + name="🔧 Other", + value=( + f"`{config.BOT_PREFIX}help` - Show this help message\n" + f"`{config.BOT_PREFIX}status` - Check Jellyseerr server status and authentication\n" + f"`{config.BOT_PREFIX}testmedia ` - Test if media exists in TMDB and Jellyseerr\n" + ), + inline=False + ) + + embed.set_footer(text=f"Bot Prefix: {config.BOT_PREFIX}") + + await ctx.send(embed=embed) + + @commands.command(name="testmedia") + async def test_media(self, ctx, media_type: str = None, media_id = None): + """ + Test if a media ID exists directly on TMDB and check correct media type + Example: !testmedia movie 83936 + """ + if not media_type and media_id: + # If only one parameter is provided, assume it's the ID + media_id = media_type + media_type = None + await ctx.send(f"Testing both movie and TV show for ID {media_id}...") + elif not media_type or not media_id: + await ctx.send("Please provide a media ID. Example: `!testmedia 83936` or `!testmedia movie 550`") + return + + if media_type and media_type.lower() not in ['movie', 'tv']: + await ctx.send("Invalid media type. Use 'movie' or 'tv' or just provide the ID to test both.") + return + + media_id = safe_int_convert(media_id) + if not media_id: + await ctx.send(f"Please provide a valid media ID. Example: `!testmedia 550`") + return + + async with ctx.typing(): + try: + # Try to get media info from Jellyseerr + api = get_api() + + embed = discord.Embed( + title=f"Media Test: ID {media_id}", + description="Testing this ID against both movie and TV endpoints", + color=self.embed_color + ) + + # Test both movie and TV endpoints directly using HTTP requests + async with aiohttp.ClientSession() as session: + # Test movie endpoint + async with session.get(f"{self.tmdb_base_url}/movie/{media_id}?api_key=12345") as movie_response: + movie_status = movie_response.status + + # Test TV endpoint + async with session.get(f"{self.tmdb_base_url}/tv/{media_id}?api_key=12345") as tv_response: + tv_status = tv_response.status + + # Check which type of media this ID belongs to + is_movie = movie_status == 401 # 401 means it exists but auth failed + is_tv = tv_status == 401 # 401 means it exists but auth failed + + if is_movie and not is_tv: + embed.add_field( + name="TMDB Status", + value="✅ This ID belongs to a **MOVIE**", + inline=False + ) + correct_type = "movie" + elif is_tv and not is_movie: + embed.add_field( + name="TMDB Status", + value="✅ This ID belongs to a **TV SHOW**", + inline=False + ) + correct_type = "tv" + elif is_movie and is_tv: + embed.add_field( + name="TMDB Status", + value="⚠️ Unusual: This ID seems to exist as both movie and TV show", + inline=False + ) + correct_type = "both" + else: + embed.add_field( + name="TMDB Status", + value="❌ This ID was not found in TMDB database (neither movie nor TV show)", + inline=False + ) + correct_type = None + + # If media_type was specified, check if it matches + if media_type and correct_type and correct_type != "both" and media_type.lower() != correct_type: + embed.add_field( + name="Media Type Mismatch", + value=f"⚠️ You specified `{media_type}` but this ID is for a `{correct_type}`.\n" + f"Use `{config.BOT_PREFIX}{correct_type} {media_id}` instead.", + inline=False + ) + + # Test Jellyseerr access for the correct media type + try: + if correct_type == "movie" or (media_type and media_type.lower() == 'movie'): + media_details = await api.get_movie_details(media_id) + title = media_details.get('title', 'Unknown') + embed.add_field( + name="Jellyseerr Movie Status", + value=f"✅ Found in Jellyseerr: {title}", + inline=False + ) + elif correct_type == "tv" or (media_type and media_type.lower() == 'tv'): + media_details = await api.get_tv_details(media_id) + title = media_details.get('name', 'Unknown') + embed.add_field( + name="Jellyseerr TV Status", + value=f"✅ Found in Jellyseerr: {title}", + inline=False + ) + elif correct_type == "both": + # Try both endpoints + try: + movie_details = await api.get_movie_details(media_id) + movie_title = movie_details.get('title', 'Unknown') + embed.add_field( + name="Jellyseerr Movie Status", + value=f"✅ Found as movie in Jellyseerr: {movie_title}", + inline=False + ) + except: + embed.add_field( + name="Jellyseerr Movie Status", + value="❌ Not found as movie in Jellyseerr", + inline=False + ) + + try: + tv_details = await api.get_tv_details(media_id) + tv_title = tv_details.get('name', 'Unknown') + embed.add_field( + name="Jellyseerr TV Status", + value=f"✅ Found as TV show in Jellyseerr: {tv_title}", + inline=False + ) + except: + embed.add_field( + name="Jellyseerr TV Status", + value="❌ Not found as TV show in Jellyseerr", + inline=False + ) + else: # No correct type identified + # Try both if we couldn't determine the type + tv_success = movie_success = False + try: + movie_details = await api.get_movie_details(media_id) + if movie_details.get('title'): + embed.add_field( + name="Jellyseerr Status (Unexpected)", + value=f"✅ Found as MOVIE in Jellyseerr: {movie_details.get('title')}", + inline=False + ) + movie_success = True + except: + pass + + try: + tv_details = await api.get_tv_details(media_id) + if tv_details.get('name'): + embed.add_field( + name="Jellyseerr Status (Unexpected)", + value=f"✅ Found as TV SHOW in Jellyseerr: {tv_details.get('name')}", + inline=False + ) + tv_success = True + except: + pass + + if not tv_success and not movie_success: + embed.add_field( + name="Jellyseerr Status", + value="❌ Not found in Jellyseerr (neither as movie nor TV show)", + inline=False + ) + + except Exception as e: + embed.add_field( + name="Jellyseerr Status", + value=f"❌ Error retrieving from Jellyseerr: {str(e)}", + inline=False + ) + + # Add recommendations + if not correct_type: + embed.add_field( + name="Recommendation", + value="This media ID doesn't exist in TMDB. Try searching for the correct ID with:\n" + f"`{config.BOT_PREFIX}search movie name` or `{config.BOT_PREFIX}search tv name`", + inline=False + ) + elif "Error retrieving" in ''.join([f.value for f in embed.fields if hasattr(f, 'value')]): + embed.add_field( + name="Recommendation", + value="The media exists in TMDB but your Jellyseerr account cannot access it. This could be due to:\n" + "• Permission settings in your Jellyseerr account\n" + "• Content filtering in Jellyseerr\n" + "• The media may be excluded from your Jellyseerr instance", + inline=False + ) + elif correct_type == "movie": + embed.add_field( + name="Command to Use", + value=f"Use `{config.BOT_PREFIX}movie {media_id}` to get details\n" + f"Use `{config.BOT_PREFIX}request movie {media_id}` to request it", + inline=False + ) + elif correct_type == "tv": + embed.add_field( + name="Command to Use", + value=f"Use `{config.BOT_PREFIX}tv {media_id}` to get details\n" + f"Use `{config.BOT_PREFIX}request tv {media_id}` to request it", + inline=False + ) + + await ctx.send(embed=embed) + + except Exception as e: + logger.error(f"Error in test_media command: {str(e)}", exc_info=True) + await ctx.send(f"Error testing media: {str(e)}") + + @commands.command(name="status") + async def get_status(self, ctx): + """Check Jellyseerr server status""" + async with ctx.typing(): + try: + api = get_api() + status_data = await api._request('GET', '/status') + + embed = discord.Embed( + title="Jellyseerr Server Status", + color=self.embed_color + ) + + version = status_data.get('version', 'Unknown') + update_available = status_data.get('updateAvailable', False) + commits_behind = status_data.get('commitsBehind', 0) + + embed.add_field(name="Version", value=version, inline=True) + embed.add_field(name="Update Available", value="Yes" if update_available else "No", inline=True) + + if update_available: + embed.add_field(name="Commits Behind", value=str(commits_behind), inline=True) + + embed.add_field(name="Server URL", value=config.JELLYSEERR_URL, inline=False) + embed.add_field(name="Bot Account", value=config.JELLYSEERR_EMAIL, inline=False) + + # Check if authenticated + try: + api = get_api() + if api and api.last_login: + embed.add_field(name="Authentication", value="✅ Authenticated", inline=True) + else: + embed.add_field(name="Authentication", value="❌ Not authenticated", inline=True) + except: + embed.add_field(name="Authentication", value="❌ Not authenticated", inline=True) + + await ctx.send(embed=embed) + + except Exception as e: + await ctx.send(f"Error checking server status: {str(e)}") \ No newline at end of file