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): """Commands for utility functions like help, status, and testing.""" def __init__(self, bot): """Initialize the UtilityCommands cog. Args: bot: The Discord bot instance """ 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. Args: ctx: The command context command: Optional specific command to get help for """ 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. This command tests whether a given ID exists on TMDB and Jellyseerr, and verifies the correct media type. Useful for troubleshooting. Args: ctx: The command context media_type: Optional media type (movie or tv) media_id: The TMDB ID to test Example: !testmedia movie 83936 """ if not media_type and not media_id: await ctx.send("Please provide a media ID. Example: `!testmedia 83936` or `!testmedia movie 550`") return elif not 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}...") 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 or media_id <= 0: await ctx.send(f"Please provide a valid media ID (a positive number). 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: try: # Test movie endpoint async with session.get(f"{self.tmdb_base_url}/movie/{media_id}", timeout=aiohttp.ClientTimeout(total=15)) as movie_response: movie_status = movie_response.status # Test TV endpoint async with session.get(f"{self.tmdb_base_url}/tv/{media_id}", timeout=aiohttp.ClientTimeout(total=15)) as tv_response: tv_status = tv_response.status # Check which type of media this ID belongs to # 401 or 404 with specific message means it exists but auth failed # Pure 404 means it doesn't exist is_movie = movie_status == 401 or (movie_status == 404 and "authentication" in await movie_response.text().lower()) is_tv = tv_status == 401 or (tv_status == 404 and "authentication" in await tv_response.text().lower()) except asyncio.TimeoutError: logger.error(f"TMDB request timed out for ID {media_id}", exc_info=True) embed.add_field( name="TMDB Status", value="❌ Request to TMDB timed out. Please try again later.", inline=False ) return await interaction.followup.send(embed=embed) except aiohttp.ClientConnectorError as e: logger.error(f"TMDB connection error for ID {media_id}: {str(e)}", exc_info=True) embed.add_field( name="TMDB Status", value="❌ Could not connect to TMDB. Please check your internet connection.", inline=False ) return await interaction.followup.send(embed=embed) except Exception as e: embed.add_field( name="TMDB Status", value=f"❌ Error checking TMDB: {str(e)}", inline=False ) return await interaction.followup.send(embed=embed) 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 and authentication. Provides information about the Jellyseerr server, including version, update status, and authentication status. Args: ctx: The command context """ 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 aiohttp.ClientConnectorError as e: logger.error(f"Connection error while checking server status: {str(e)}", exc_info=True) await ctx.send(f"Error connecting to Jellyseerr server at {config.JELLYSEERR_URL}. Please check if the server is running.") except asyncio.TimeoutError: logger.error("Status request timed out", exc_info=True) await ctx.send(f"Request timed out. The Jellyseerr server at {config.JELLYSEERR_URL} might be slow or overloaded.") except Exception as e: logger.error(f"Error checking server status: {str(e)}", exc_info=True) await ctx.send(f"Error checking server status: {str(e)}")