Compare commits
	
		
			2 Commits
		
	
	
		
			4281aafc61
			...
			8aa5563847
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					8aa5563847 | ||
| 
						 | 
					fe567952b4 | 
							
								
								
									
										50
									
								
								.env.example
									
									
									
									
									
								
							
							
						
						
									
										50
									
								
								.env.example
									
									
									
									
									
								
							@@ -1,36 +1,56 @@
 | 
			
		||||
# Discord Jellyseerr Bot - Environment Variables
 | 
			
		||||
#=====================================================================
 | 
			
		||||
# Discord Jellyseerr Bot - Environment Configuration
 | 
			
		||||
#=====================================================================
 | 
			
		||||
# Copy this file to .env and fill in your values
 | 
			
		||||
# All sensitive information should be kept secure
 | 
			
		||||
 | 
			
		||||
# Discord Bot Token (required)
 | 
			
		||||
#---------------------------------------------------------------------
 | 
			
		||||
# Discord Bot Settings (REQUIRED)
 | 
			
		||||
#---------------------------------------------------------------------
 | 
			
		||||
# Your Discord Bot Token from the Discord Developer Portal
 | 
			
		||||
# Create one at: https://discord.com/developers/applications
 | 
			
		||||
DISCORD_BOT_TOKEN=your_discord_bot_token_here
 | 
			
		||||
 | 
			
		||||
# Bot command prefix (default is !)
 | 
			
		||||
# Command prefix for bot commands (default: !)
 | 
			
		||||
BOT_PREFIX=!
 | 
			
		||||
 | 
			
		||||
# Jellyseerr Configuration (required)
 | 
			
		||||
# Color for embeds in Discord messages (hex format)
 | 
			
		||||
# Default: 0x3498db (Discord blue)
 | 
			
		||||
EMBED_COLOR=0x3498db
 | 
			
		||||
 | 
			
		||||
#---------------------------------------------------------------------
 | 
			
		||||
# Jellyseerr Connection Settings (REQUIRED)
 | 
			
		||||
#---------------------------------------------------------------------
 | 
			
		||||
# URL of your Jellyseerr instance including protocol and port
 | 
			
		||||
# Example: http://localhost:5055 or https://jellyseerr.yourdomain.com
 | 
			
		||||
JELLYSEERR_URL=http://your-jellyseerr-instance:5055
 | 
			
		||||
 | 
			
		||||
# Jellyseerr Authentication (using local user account)
 | 
			
		||||
# Authentication Settings (using local Jellyseerr 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)
 | 
			
		||||
# When disabled, some functionality may be limited
 | 
			
		||||
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
 | 
			
		||||
#---------------------------------------------------------------------
 | 
			
		||||
# NOTE: For notifications about media requests and availability,
 | 
			
		||||
# use Jellyseerr's built-in Discord webhook integration.
 | 
			
		||||
# Configure webhooks directly in the Jellyseerr web interface
 | 
			
		||||
# under Settings > Notifications.
 | 
			
		||||
 | 
			
		||||
#---------------------------------------------------------------------
 | 
			
		||||
# Performance & Debug Settings
 | 
			
		||||
#---------------------------------------------------------------------
 | 
			
		||||
# API request timeout in seconds
 | 
			
		||||
# Increase this value if your Jellyseerr instance is slow to respond
 | 
			
		||||
REQUEST_TIMEOUT=30
 | 
			
		||||
 | 
			
		||||
# Set to true to enable verbose logging (useful for troubleshooting)
 | 
			
		||||
# Warning: Debug mode generates large log files and exposes sensitive data
 | 
			
		||||
DEBUG_MODE=false
 | 
			
		||||
							
								
								
									
										192
									
								
								commands.py
									
									
									
									
									
								
							
							
						
						
									
										192
									
								
								commands.py
									
									
									
									
									
								
							@@ -16,6 +16,17 @@ import sys
 | 
			
		||||
jellyseerr_api = None
 | 
			
		||||
 | 
			
		||||
def get_api():
 | 
			
		||||
    """Get the Jellyseerr API client instance.
 | 
			
		||||
    
 | 
			
		||||
    This function retrieves the API client instance that was initialized in main.py.
 | 
			
		||||
    It handles the potential circular import issue by accessing the instance through sys.modules.
 | 
			
		||||
    
 | 
			
		||||
    Returns:
 | 
			
		||||
        JellyseerrAPI: The initialized API client instance
 | 
			
		||||
    
 | 
			
		||||
    Raises:
 | 
			
		||||
        RuntimeError: If the API client hasn't been initialized
 | 
			
		||||
    """
 | 
			
		||||
    global jellyseerr_api
 | 
			
		||||
    if jellyseerr_api is None:
 | 
			
		||||
        # Get the API client instance from the main module
 | 
			
		||||
@@ -25,10 +36,19 @@ def get_api():
 | 
			
		||||
        else:
 | 
			
		||||
            # This should not happen, but just in case
 | 
			
		||||
            logger.error("Could not find jellyseerr_api instance")
 | 
			
		||||
            raise RuntimeError("API client not initialized. This is likely a bug.")
 | 
			
		||||
    return jellyseerr_api
 | 
			
		||||
 | 
			
		||||
def safe_int_convert(value, default=None):
 | 
			
		||||
    """Safely convert a value to integer, returning default if conversion fails"""
 | 
			
		||||
    """Safely convert a value to integer, returning default if conversion fails.
 | 
			
		||||
    
 | 
			
		||||
    Args:
 | 
			
		||||
        value: The value to convert to an integer
 | 
			
		||||
        default: The default value to return if conversion fails
 | 
			
		||||
        
 | 
			
		||||
    Returns:
 | 
			
		||||
        int or default: The converted integer or the default value
 | 
			
		||||
    """
 | 
			
		||||
    if value is None:
 | 
			
		||||
        return default
 | 
			
		||||
    try:
 | 
			
		||||
@@ -37,13 +57,25 @@ def safe_int_convert(value, default=None):
 | 
			
		||||
        return default
 | 
			
		||||
 | 
			
		||||
class MediaCommands(commands.Cog):
 | 
			
		||||
    """Commands for searching and displaying media information."""
 | 
			
		||||
    
 | 
			
		||||
    def __init__(self, bot):
 | 
			
		||||
        """Initialize the MediaCommands cog.
 | 
			
		||||
        
 | 
			
		||||
        Args:
 | 
			
		||||
            bot: The Discord bot instance
 | 
			
		||||
        """
 | 
			
		||||
        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)"""
 | 
			
		||||
        """Get detailed information about a movie or TV show by ID (auto-detects type).
 | 
			
		||||
        
 | 
			
		||||
        Args:
 | 
			
		||||
            ctx: The command context
 | 
			
		||||
            media_id: The TMDB ID of the movie or TV show
 | 
			
		||||
        """
 | 
			
		||||
        if not media_id:
 | 
			
		||||
            await ctx.send("Please provide a media ID. Example: `!info 550` (for Fight Club)")
 | 
			
		||||
            return
 | 
			
		||||
@@ -105,8 +137,32 @@ class MediaCommands(commands.Cog):
 | 
			
		||||
                            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:
 | 
			
		||||
                        # Add request button info or status
 | 
			
		||||
                        if not media_info:
 | 
			
		||||
                            embed.add_field(
 | 
			
		||||
                                name="Request this Movie",
 | 
			
		||||
                                value=f"Use `{config.BOT_PREFIX}request movie {media_id}` to request this movie",
 | 
			
		||||
                                inline=False
 | 
			
		||||
                            )
 | 
			
		||||
                        elif media_info.get('status') == 5:  # Available
 | 
			
		||||
                            embed.add_field(
 | 
			
		||||
                                name="Status",
 | 
			
		||||
                                value="✅ This movie is available in your library",
 | 
			
		||||
                                inline=False
 | 
			
		||||
                            )
 | 
			
		||||
                        elif media_info.get('status') == 3:  # Processing
 | 
			
		||||
                            embed.add_field(
 | 
			
		||||
                                name="Status",
 | 
			
		||||
                                value="⏳ This movie is being processed",
 | 
			
		||||
                                inline=False
 | 
			
		||||
                            )
 | 
			
		||||
                        elif media_info.get('status') == 2:  # Pending
 | 
			
		||||
                            embed.add_field(
 | 
			
		||||
                                name="Status",
 | 
			
		||||
                                value="⏳ This movie has been requested and is pending",
 | 
			
		||||
                                inline=False
 | 
			
		||||
                            )
 | 
			
		||||
                        elif 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",
 | 
			
		||||
@@ -198,8 +254,44 @@ class MediaCommands(commands.Cog):
 | 
			
		||||
                                    inline=False
 | 
			
		||||
                                )
 | 
			
		||||
                        
 | 
			
		||||
                        # Add request button info
 | 
			
		||||
                        if not media_info or media_info.get('status') < 5:
 | 
			
		||||
                        # Add request button info or status
 | 
			
		||||
                        if not media_info:
 | 
			
		||||
                            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} <season_nums>` for specific seasons",
 | 
			
		||||
                                inline=False
 | 
			
		||||
                            )
 | 
			
		||||
                        elif media_info.get('status') == 5:  # Available
 | 
			
		||||
                            embed.add_field(
 | 
			
		||||
                                name="Status",
 | 
			
		||||
                                value="✅ This TV show is available in your library",
 | 
			
		||||
                                inline=False
 | 
			
		||||
                            )
 | 
			
		||||
                        elif media_info.get('status') == 4:  # Partially Available
 | 
			
		||||
                            embed.add_field(
 | 
			
		||||
                                name="Status",
 | 
			
		||||
                                value="⚠️ This TV show is partially available. You may want to request specific seasons.",
 | 
			
		||||
                                inline=False
 | 
			
		||||
                            )
 | 
			
		||||
                            embed.add_field(
 | 
			
		||||
                                name="Request Specific Seasons",
 | 
			
		||||
                                value=f"Use `{config.BOT_PREFIX}request tv {media_id} <season_nums>` to request specific seasons",
 | 
			
		||||
                                inline=False
 | 
			
		||||
                            )
 | 
			
		||||
                        elif media_info.get('status') == 3:  # Processing
 | 
			
		||||
                            embed.add_field(
 | 
			
		||||
                                name="Status",
 | 
			
		||||
                                value="⏳ This TV show is being processed",
 | 
			
		||||
                                inline=False
 | 
			
		||||
                            )
 | 
			
		||||
                        elif media_info.get('status') == 2:  # Pending
 | 
			
		||||
                            embed.add_field(
 | 
			
		||||
                                name="Status",
 | 
			
		||||
                                value="⏳ This TV show has been requested and is pending",
 | 
			
		||||
                                inline=False
 | 
			
		||||
                            )
 | 
			
		||||
                        elif 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"
 | 
			
		||||
@@ -219,13 +311,24 @@ class MediaCommands(commands.Cog):
 | 
			
		||||
                               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 aiohttp.ClientConnectorError as e:
 | 
			
		||||
                logger.error(f"Connection error retrieving media details for ID {media_id}: {str(e)}", exc_info=True)
 | 
			
		||||
                await ctx.send(f"Error connecting to Jellyseerr server. Please check if the server is running and try again later.")
 | 
			
		||||
            except asyncio.TimeoutError:
 | 
			
		||||
                logger.error(f"Request timed out for media ID {media_id}", exc_info=True)
 | 
			
		||||
                await ctx.send(f"Request timed out. The Jellyseerr server might be slow or overloaded. Please try again later.")
 | 
			
		||||
            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"""
 | 
			
		||||
        """Search for movies and TV shows by title.
 | 
			
		||||
        
 | 
			
		||||
        Args:
 | 
			
		||||
            ctx: The command context
 | 
			
		||||
            query: The search query/term
 | 
			
		||||
        """
 | 
			
		||||
        if not query:
 | 
			
		||||
            await ctx.send("Please provide a search term. Example: `!search Stranger Things`")
 | 
			
		||||
            return
 | 
			
		||||
@@ -282,14 +385,25 @@ class MediaCommands(commands.Cog):
 | 
			
		||||
                
 | 
			
		||||
                await ctx.send(embed=embed)
 | 
			
		||||
            
 | 
			
		||||
            except aiohttp.ClientConnectorError:
 | 
			
		||||
                logger.error(f"Connection error while searching for '{query}'", exc_info=True)
 | 
			
		||||
                await ctx.send(f"Error connecting to Jellyseerr server. Please check if the server is running and try again later.")
 | 
			
		||||
            except asyncio.TimeoutError:
 | 
			
		||||
                logger.error(f"Search request timed out for '{query}'", exc_info=True)
 | 
			
		||||
                await ctx.send(f"Search request timed out. The Jellyseerr server might be slow or overloaded. Please try again later.")
 | 
			
		||||
            except Exception as e:
 | 
			
		||||
                logger.error(f"Error searching for '{query}': {str(e)}", exc_info=True)
 | 
			
		||||
                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"""
 | 
			
		||||
        """Get trending movies and TV shows.
 | 
			
		||||
        
 | 
			
		||||
        Args:
 | 
			
		||||
            ctx: The command context
 | 
			
		||||
        """
 | 
			
		||||
        async with ctx.typing():
 | 
			
		||||
            try:
 | 
			
		||||
                api = get_api()
 | 
			
		||||
@@ -333,11 +447,25 @@ class MediaCommands(commands.Cog):
 | 
			
		||||
                
 | 
			
		||||
                await ctx.send(embed=embed)
 | 
			
		||||
            
 | 
			
		||||
            except aiohttp.ClientConnectorError:
 | 
			
		||||
                logger.error("Connection error while retrieving trending media", exc_info=True)
 | 
			
		||||
                await ctx.send(f"Error connecting to Jellyseerr server. Please check if the server is running and try again later.")
 | 
			
		||||
            except asyncio.TimeoutError:
 | 
			
		||||
                logger.error("Trending request timed out", exc_info=True)
 | 
			
		||||
                await ctx.send(f"Request timed out. The Jellyseerr server might be slow or overloaded. Please try again later.")
 | 
			
		||||
            except Exception as e:
 | 
			
		||||
                logger.error(f"Error retrieving trending media: {str(e)}", exc_info=True)
 | 
			
		||||
                await ctx.send(f"Error retrieving trending media: {str(e)}")
 | 
			
		||||
 | 
			
		||||
class RequestCommands(commands.Cog):
 | 
			
		||||
    """Commands for managing media requests."""
 | 
			
		||||
    
 | 
			
		||||
    def __init__(self, bot):
 | 
			
		||||
        """Initialize the RequestCommands cog.
 | 
			
		||||
        
 | 
			
		||||
        Args:
 | 
			
		||||
            bot: The Discord bot instance
 | 
			
		||||
        """
 | 
			
		||||
        self.bot = bot
 | 
			
		||||
        self.embed_color = config.EMBED_COLOR
 | 
			
		||||
    
 | 
			
		||||
@@ -418,6 +546,22 @@ class RequestCommands(commands.Cog):
 | 
			
		||||
                    await ctx.send(f"Could not retrieve details for {media_type} with ID {media_id}. Please check the ID and try again.")
 | 
			
		||||
                    return
 | 
			
		||||
                
 | 
			
		||||
                # Check if media is already available
 | 
			
		||||
                if media_details.get('mediaInfo'):
 | 
			
		||||
                    status = media_details.get('mediaInfo', {}).get('status')
 | 
			
		||||
                    if status == 5:  # 5 = Available
 | 
			
		||||
                        await ctx.send(f"**{title}** is already available in your library! No need to request it.")
 | 
			
		||||
                        return
 | 
			
		||||
                    elif status == 4:  # 4 = Partially Available (for TV shows)
 | 
			
		||||
                        if media_type.lower() == 'tv':
 | 
			
		||||
                            await ctx.send(f"**{title}** is partially available in your library. You might want to request specific seasons instead.")
 | 
			
		||||
                    elif status == 3:  # 3 = Processing
 | 
			
		||||
                        await ctx.send(f"**{title}** is currently being processed. Please wait for it to become available.")
 | 
			
		||||
                        return
 | 
			
		||||
                    elif status == 2:  # 2 = Pending
 | 
			
		||||
                        await ctx.send(f"**{title}** has already been requested and is pending approval or processing.")
 | 
			
		||||
                        return
 | 
			
		||||
                
 | 
			
		||||
                # Create the request
 | 
			
		||||
                request_data = {
 | 
			
		||||
                    'media_type': media_type.lower(),
 | 
			
		||||
@@ -473,15 +617,25 @@ class RequestCommands(commands.Cog):
 | 
			
		||||
                
 | 
			
		||||
                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")
 | 
			
		||||
                    if "Request already exists" in error_message:
 | 
			
		||||
                        await ctx.send(f"This media has already been requested and is either pending or being processed.")
 | 
			
		||||
                    else:
 | 
			
		||||
                        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 using `{config.BOT_PREFIX}info {media_id}` to check the current status")
 | 
			
		||||
                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)}")
 | 
			
		||||
                    # Provide more specific error messages for common issues
 | 
			
		||||
                    if "already exists" in str(e).lower():
 | 
			
		||||
                        await ctx.send(f"This media has already been requested or is already available in your library.")
 | 
			
		||||
                    elif "not found" in str(e).lower():
 | 
			
		||||
                        await ctx.send(f"Media not found. Please check the ID and try again.")
 | 
			
		||||
                    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):
 | 
			
		||||
@@ -571,7 +725,14 @@ class RequestCommands(commands.Cog):
 | 
			
		||||
                
 | 
			
		||||
                await ctx.send(embed=embed)
 | 
			
		||||
            
 | 
			
		||||
            except aiohttp.ClientConnectorError:
 | 
			
		||||
                logger.error(f"Connection error while retrieving {status} requests", exc_info=True)
 | 
			
		||||
                await ctx.send(f"Error connecting to Jellyseerr server. Please check if the server is running and try again later.")
 | 
			
		||||
            except asyncio.TimeoutError:
 | 
			
		||||
                logger.error(f"Request timed out while retrieving {status} requests", exc_info=True)
 | 
			
		||||
                await ctx.send(f"Request timed out. The Jellyseerr server might be slow or overloaded. Please try again later.")
 | 
			
		||||
            except Exception as e:
 | 
			
		||||
                logger.error(f"Error retrieving {status} requests: {str(e)}", exc_info=True)
 | 
			
		||||
                await ctx.send(f"Error retrieving requests: {str(e)}")
 | 
			
		||||
    
 | 
			
		||||
    # Approve and decline commands have been removed
 | 
			
		||||
@@ -579,6 +740,11 @@ class RequestCommands(commands.Cog):
 | 
			
		||||
# UtilityCommands have been moved to utility_commands.py
 | 
			
		||||
 | 
			
		||||
def setup(bot):
 | 
			
		||||
    """Set up the command cogs.
 | 
			
		||||
    
 | 
			
		||||
    Args:
 | 
			
		||||
        bot: The Discord bot instance
 | 
			
		||||
    """
 | 
			
		||||
    bot.add_cog(MediaCommands(bot))
 | 
			
		||||
    bot.add_cog(RequestCommands(bot))
 | 
			
		||||
    bot.add_cog(UtilityCommands(bot))
 | 
			
		||||
@@ -10,17 +10,29 @@ from typing import Dict, List, Optional, Any, Union
 | 
			
		||||
logger = logging.getLogger('jellyseerr_api')
 | 
			
		||||
 | 
			
		||||
class JellyseerrAPI:
 | 
			
		||||
    """Client for interacting with the Jellyseerr API.
 | 
			
		||||
    
 | 
			
		||||
    This class handles authentication, session management, and provides methods
 | 
			
		||||
    for interacting with various Jellyseerr API endpoints.
 | 
			
		||||
    """
 | 
			
		||||
    def __init__(self, base_url: str, email: str = None, password: str = None):
 | 
			
		||||
        """Initialize the Jellyseerr API client.
 | 
			
		||||
        
 | 
			
		||||
        Args:
 | 
			
		||||
            base_url: The base URL of the Jellyseerr instance
 | 
			
		||||
            email: Email for authentication with Jellyseerr
 | 
			
		||||
            password: Password for authentication with Jellyseerr
 | 
			
		||||
        """
 | 
			
		||||
        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
 | 
			
		||||
        self.session: Optional[aiohttp.ClientSession] = None
 | 
			
		||||
        self.cookie_jar: Optional[aiohttp.CookieJar] = None
 | 
			
		||||
        self.auth_cookie: Optional[str] = None
 | 
			
		||||
        self.last_login: Optional[datetime] = None
 | 
			
		||||
 | 
			
		||||
    async def __aenter__(self):
 | 
			
		||||
        if not self.cookie_jar:
 | 
			
		||||
@@ -34,7 +46,14 @@ class JellyseerrAPI:
 | 
			
		||||
            await self.session.close()
 | 
			
		||||
            
 | 
			
		||||
    async def login(self) -> bool:
 | 
			
		||||
        """Log in to Jellyseerr using local authentication"""
 | 
			
		||||
        """Log in to Jellyseerr using local authentication.
 | 
			
		||||
        
 | 
			
		||||
        Returns:
 | 
			
		||||
            bool: True if login was successful
 | 
			
		||||
            
 | 
			
		||||
        Raises:
 | 
			
		||||
            Exception: If authentication fails for any reason
 | 
			
		||||
        """
 | 
			
		||||
        # 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 
 | 
			
		||||
@@ -103,7 +122,21 @@ class JellyseerrAPI:
 | 
			
		||||
            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:
 | 
			
		||||
    async def _request(self, method: str, endpoint: str, params: Optional[Dict[str, Any]] = None, data: Optional[Dict[str, Any]] = None) -> Any:
 | 
			
		||||
        """Make a request to the Jellyseerr API.
 | 
			
		||||
        
 | 
			
		||||
        Args:
 | 
			
		||||
            method: HTTP method (GET, POST, etc.)
 | 
			
		||||
            endpoint: API endpoint (without the base URL and /api/v1)
 | 
			
		||||
            params: Query parameters
 | 
			
		||||
            data: JSON request body
 | 
			
		||||
            
 | 
			
		||||
        Returns:
 | 
			
		||||
            Any: The JSON response data, or None for 204 responses
 | 
			
		||||
            
 | 
			
		||||
        Raises:
 | 
			
		||||
            Exception: For API errors, connection issues, or other failures
 | 
			
		||||
        """
 | 
			
		||||
        # Ensure we're logged in
 | 
			
		||||
        if not self.last_login:
 | 
			
		||||
            try:
 | 
			
		||||
@@ -127,9 +160,8 @@ class JellyseerrAPI:
 | 
			
		||||
 | 
			
		||||
        url = f"{self.base_url}/api/v1{endpoint}"
 | 
			
		||||
        
 | 
			
		||||
        # URL encode parameters
 | 
			
		||||
        # URL encode parameters - only encode string values, not numbers or booleans
 | 
			
		||||
        if params:
 | 
			
		||||
            # Create a new dict with URL-encoded values
 | 
			
		||||
            encoded_params = {}
 | 
			
		||||
            for key, value in params.items():
 | 
			
		||||
                if isinstance(value, str):
 | 
			
		||||
@@ -139,8 +171,17 @@ class JellyseerrAPI:
 | 
			
		||||
            params = encoded_params
 | 
			
		||||
        
 | 
			
		||||
        try:
 | 
			
		||||
            # Set timeout for the request
 | 
			
		||||
            timeout = aiohttp.ClientTimeout(total=config.REQUEST_TIMEOUT)
 | 
			
		||||
            
 | 
			
		||||
            # Attempt the request
 | 
			
		||||
            async with self.session.request(method, url, headers=self.headers, params=params, json=data) as response:
 | 
			
		||||
            async with self.session.request(
 | 
			
		||||
                method, url, 
 | 
			
		||||
                headers=self.headers, 
 | 
			
		||||
                params=params, 
 | 
			
		||||
                json=data,
 | 
			
		||||
                timeout=timeout
 | 
			
		||||
            ) as response:
 | 
			
		||||
                if response.status == 204:  # No content
 | 
			
		||||
                    return None
 | 
			
		||||
                
 | 
			
		||||
@@ -153,7 +194,13 @@ class JellyseerrAPI:
 | 
			
		||||
                        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:
 | 
			
		||||
                            async with self.session.request(
 | 
			
		||||
                                method, url, 
 | 
			
		||||
                                headers=self.headers, 
 | 
			
		||||
                                params=params, 
 | 
			
		||||
                                json=data,
 | 
			
		||||
                                timeout=timeout
 | 
			
		||||
                            ) as retry_response:
 | 
			
		||||
                                logger.debug(f"Retry response status: {retry_response.status}")
 | 
			
		||||
                                
 | 
			
		||||
                                if retry_response.status == 204:
 | 
			
		||||
@@ -180,15 +227,34 @@ class JellyseerrAPI:
 | 
			
		||||
                        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:
 | 
			
		||||
                except aiohttp.ContentTypeError:
 | 
			
		||||
                    # Not JSON, get the text response
 | 
			
		||||
                    error_text = await response.text()
 | 
			
		||||
                    raise Exception(f"API Error ({response.status}): Not a valid JSON response: {error_text[:100]}...")
 | 
			
		||||
                except aiohttp.ClientResponseError as e:
 | 
			
		||||
                    raise Exception(f"API Error ({response.status}): {str(e)}")
 | 
			
		||||
        except aiohttp.ClientConnectorError as e:
 | 
			
		||||
            logger.error(f"Connection error: {str(e)}")
 | 
			
		||||
            raise Exception(f"Connection error: Could not connect to {self.base_url}: {str(e)}")
 | 
			
		||||
        except aiohttp.ClientError as e:
 | 
			
		||||
            logger.error(f"HTTP client error: {str(e)}")
 | 
			
		||||
            raise Exception(f"Connection error: {str(e)}")
 | 
			
		||||
        except asyncio.TimeoutError:
 | 
			
		||||
            logger.error(f"Request timed out after {config.REQUEST_TIMEOUT} seconds")
 | 
			
		||||
            raise Exception(f"Request timed out after {config.REQUEST_TIMEOUT} seconds. Check your network connection and Jellyseerr server status.")
 | 
			
		||||
 | 
			
		||||
    # Search endpoints
 | 
			
		||||
    async def search(self, query: str, page: int = 1, language: str = 'en') -> Dict:
 | 
			
		||||
        """Search for movies, TV shows, or people"""
 | 
			
		||||
    async def search(self, query: str, page: int = 1, language: str = 'en') -> Dict[str, Any]:
 | 
			
		||||
        """Search for movies, TV shows, or people.
 | 
			
		||||
        
 | 
			
		||||
        Args:
 | 
			
		||||
            query: The search term
 | 
			
		||||
            page: Page number for results
 | 
			
		||||
            language: Language code for results (e.g., 'en', 'fr')
 | 
			
		||||
            
 | 
			
		||||
        Returns:
 | 
			
		||||
            Dict containing search results
 | 
			
		||||
        """
 | 
			
		||||
        params = {
 | 
			
		||||
            'query': query,
 | 
			
		||||
            'page': page,
 | 
			
		||||
@@ -197,32 +263,75 @@ class JellyseerrAPI:
 | 
			
		||||
        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"""
 | 
			
		||||
    async def get_movie_details(self, movie_id: int, language: str = 'en') -> Dict[str, Any]:
 | 
			
		||||
        """Get detailed information about a movie.
 | 
			
		||||
        
 | 
			
		||||
        Args:
 | 
			
		||||
            movie_id: TMDB ID of the movie
 | 
			
		||||
            language: Language code for results (e.g., 'en', 'fr')
 | 
			
		||||
            
 | 
			
		||||
        Returns:
 | 
			
		||||
            Dict containing movie details
 | 
			
		||||
        """
 | 
			
		||||
        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"""
 | 
			
		||||
    async def get_tv_details(self, tv_id: int, language: str = 'en') -> Dict[str, Any]:
 | 
			
		||||
        """Get detailed information about a TV show.
 | 
			
		||||
        
 | 
			
		||||
        Args:
 | 
			
		||||
            tv_id: TMDB ID of the TV show
 | 
			
		||||
            language: Language code for results (e.g., 'en', 'fr')
 | 
			
		||||
            
 | 
			
		||||
        Returns:
 | 
			
		||||
            Dict containing TV show details
 | 
			
		||||
        """
 | 
			
		||||
        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"""
 | 
			
		||||
    async def get_season_details(self, tv_id: int, season_id: int, language: str = 'en') -> Dict[str, Any]:
 | 
			
		||||
        """Get detailed information about a TV season.
 | 
			
		||||
        
 | 
			
		||||
        Args:
 | 
			
		||||
            tv_id: TMDB ID of the TV show
 | 
			
		||||
            season_id: Season number
 | 
			
		||||
            language: Language code for results (e.g., 'en', 'fr')
 | 
			
		||||
            
 | 
			
		||||
        Returns:
 | 
			
		||||
            Dict containing season details
 | 
			
		||||
        """
 | 
			
		||||
        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"""
 | 
			
		||||
    async def get_movie_recommendations(self, movie_id: int, page: int = 1, language: str = 'en') -> Dict[str, Any]:
 | 
			
		||||
        """Get movie recommendations based on a movie.
 | 
			
		||||
        
 | 
			
		||||
        Args:
 | 
			
		||||
            movie_id: TMDB ID of the movie
 | 
			
		||||
            page: Page number for results
 | 
			
		||||
            language: Language code for results (e.g., 'en', 'fr')
 | 
			
		||||
            
 | 
			
		||||
        Returns:
 | 
			
		||||
            Dict containing recommended movies
 | 
			
		||||
        """
 | 
			
		||||
        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"""
 | 
			
		||||
    async def get_tv_recommendations(self, tv_id: int, page: int = 1, language: str = 'en') -> Dict[str, Any]:
 | 
			
		||||
        """Get TV show recommendations based on a TV show.
 | 
			
		||||
        
 | 
			
		||||
        Args:
 | 
			
		||||
            tv_id: TMDB ID of the TV show
 | 
			
		||||
            page: Page number for results
 | 
			
		||||
            language: Language code for results (e.g., 'en', 'fr')
 | 
			
		||||
            
 | 
			
		||||
        Returns:
 | 
			
		||||
            Dict containing recommended TV shows
 | 
			
		||||
        """
 | 
			
		||||
        params = {
 | 
			
		||||
            'page': page,
 | 
			
		||||
            'language': language
 | 
			
		||||
@@ -241,8 +350,21 @@ class JellyseerrAPI:
 | 
			
		||||
 | 
			
		||||
    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"""
 | 
			
		||||
                           is_4k: bool = False) -> Dict[str, Any]:
 | 
			
		||||
        """Create a new media request.
 | 
			
		||||
        
 | 
			
		||||
        Args:
 | 
			
		||||
            media_type: Type of media ('movie' or 'tv')
 | 
			
		||||
            media_id: TMDB ID of the media
 | 
			
		||||
            seasons: For TV shows, specific seasons to request or 'all'
 | 
			
		||||
            is_4k: Whether to request 4K version
 | 
			
		||||
            
 | 
			
		||||
        Returns:
 | 
			
		||||
            Dict containing the created request information
 | 
			
		||||
            
 | 
			
		||||
        Raises:
 | 
			
		||||
            Exception: If the request creation fails
 | 
			
		||||
        """
 | 
			
		||||
        data = {
 | 
			
		||||
            'mediaType': media_type,
 | 
			
		||||
            'mediaId': media_id,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										88
									
								
								main.py
									
									
									
									
									
								
							
							
						
						
									
										88
									
								
								main.py
									
									
									
									
									
								
							@@ -10,9 +10,14 @@ import traceback
 | 
			
		||||
 | 
			
		||||
# Set up logging
 | 
			
		||||
def setup_logging():
 | 
			
		||||
    """Configure and set up the logging system for the bot.
 | 
			
		||||
    
 | 
			
		||||
    Returns:
 | 
			
		||||
        logging.Logger: The configured logger for the bot
 | 
			
		||||
    """
 | 
			
		||||
    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
 | 
			
		||||
    log_level = logging.DEBUG if config.DEBUG_MODE else logging.INFO
 | 
			
		||||
    
 | 
			
		||||
    # Create logs directory if it doesn't exist
 | 
			
		||||
    os.makedirs('logs', exist_ok=True)
 | 
			
		||||
@@ -21,9 +26,14 @@ def setup_logging():
 | 
			
		||||
    root_logger = logging.getLogger()
 | 
			
		||||
    root_logger.setLevel(log_level)
 | 
			
		||||
    
 | 
			
		||||
    # Clear existing handlers to avoid duplication
 | 
			
		||||
    if root_logger.handlers:
 | 
			
		||||
        for handler in root_logger.handlers[:]:
 | 
			
		||||
            root_logger.removeHandler(handler)
 | 
			
		||||
    
 | 
			
		||||
    # Console handler
 | 
			
		||||
    console_handler = logging.StreamHandler(sys.stdout)
 | 
			
		||||
    console_handler.setFormatter(logging.Formatter(debug_format if config.DEBUG_MODE else log_format))
 | 
			
		||||
    console_handler.setFormatter(logging.Formatter(detailed_log_format if config.DEBUG_MODE else log_format))
 | 
			
		||||
    console_handler.setLevel(log_level)
 | 
			
		||||
    
 | 
			
		||||
    # File handler with rotation (5 files, 5MB each)
 | 
			
		||||
@@ -74,6 +84,7 @@ jellyseerr_api = None
 | 
			
		||||
 | 
			
		||||
# Load all cogs
 | 
			
		||||
async def load_extensions():
 | 
			
		||||
    """Load all command extensions and initialize the API client."""
 | 
			
		||||
    # Import here to avoid circular imports
 | 
			
		||||
    from commands import MediaCommands, RequestCommands
 | 
			
		||||
    from utility_commands import UtilityCommands
 | 
			
		||||
@@ -81,23 +92,35 @@ async def load_extensions():
 | 
			
		||||
    
 | 
			
		||||
    # Initialize the API client
 | 
			
		||||
    global jellyseerr_api
 | 
			
		||||
    jellyseerr_api = JellyseerrAPI(config.JELLYSEERR_URL, config.JELLYSEERR_EMAIL, config.JELLYSEERR_PASSWORD)
 | 
			
		||||
    try:
 | 
			
		||||
        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
 | 
			
		||||
        # 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")
 | 
			
		||||
        # Add all cogs
 | 
			
		||||
        logger.info("Loading command extensions...")
 | 
			
		||||
        await bot.add_cog(MediaCommands(bot))
 | 
			
		||||
        await bot.add_cog(RequestCommands(bot))
 | 
			
		||||
        await bot.add_cog(UtilityCommands(bot))
 | 
			
		||||
        logger.info("Loaded all extensions")
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error(f"Failed to initialize extensions: {str(e)}", exc_info=True)
 | 
			
		||||
        raise
 | 
			
		||||
 | 
			
		||||
@bot.event
 | 
			
		||||
async def on_ready():
 | 
			
		||||
    """Handler for when the bot has connected to Discord and is ready."""
 | 
			
		||||
    logger.info(f'Logged in as {bot.user} (ID: {bot.user.id})')
 | 
			
		||||
    logger.info(f'Connected to {len(bot.guilds)} guilds')
 | 
			
		||||
    
 | 
			
		||||
    # List connected guilds
 | 
			
		||||
    guild_list = []
 | 
			
		||||
    for guild in bot.guilds:
 | 
			
		||||
        guild_list.append(f'  - {guild.name} (ID: {guild.id})')
 | 
			
		||||
        logger.info(f'  - {guild.name} (ID: {guild.id})')
 | 
			
		||||
    
 | 
			
		||||
    logger.info(f'Using prefix: {config.BOT_PREFIX}')
 | 
			
		||||
    logger.info(f'Jellyseerr URL: {config.JELLYSEERR_URL}')
 | 
			
		||||
    
 | 
			
		||||
@@ -133,6 +156,20 @@ async def on_ready():
 | 
			
		||||
    await bot.change_presence(activity=activity)
 | 
			
		||||
    logger.info("Bot is ready and fully operational")
 | 
			
		||||
    
 | 
			
		||||
    # Print a nice summary to the log
 | 
			
		||||
    bot_info = [
 | 
			
		||||
        "=====================================",
 | 
			
		||||
        "Discord Jellyseerr Bot Ready",
 | 
			
		||||
        "=====================================",
 | 
			
		||||
        f"Bot User: {bot.user}",
 | 
			
		||||
        f"Bot ID: {bot.user.id}",
 | 
			
		||||
        f"Prefix: {config.BOT_PREFIX}",
 | 
			
		||||
        f"Guilds: {len(bot.guilds)}",
 | 
			
		||||
        f"API URL: {config.JELLYSEERR_URL}",
 | 
			
		||||
        "=====================================" 
 | 
			
		||||
    ]
 | 
			
		||||
    logger.info("\n".join(bot_info))
 | 
			
		||||
 | 
			
		||||
@bot.event
 | 
			
		||||
async def on_command(ctx):
 | 
			
		||||
    logger.info(f"Command '{ctx.command}' invoked by {ctx.author} in {ctx.guild}/{ctx.channel}")
 | 
			
		||||
@@ -143,6 +180,12 @@ async def on_command_completion(ctx):
 | 
			
		||||
 | 
			
		||||
@bot.event
 | 
			
		||||
async def on_command_error(ctx, error):
 | 
			
		||||
    """Handle command errors and provide user-friendly responses.
 | 
			
		||||
    
 | 
			
		||||
    Args:
 | 
			
		||||
        ctx: The context of the command
 | 
			
		||||
        error: The error that occurred
 | 
			
		||||
    """
 | 
			
		||||
    if isinstance(error, commands.CommandNotFound):
 | 
			
		||||
        logger.debug(f"Command not found: {ctx.message.content}")
 | 
			
		||||
        return
 | 
			
		||||
@@ -169,21 +212,35 @@ async def on_command_error(ctx, error):
 | 
			
		||||
        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)}"
 | 
			
		||||
        command_name = ctx.command.name if ctx.command else "unknown"
 | 
			
		||||
        error_details = f"Command error in {command_name} 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()}")
 | 
			
		||||
            try:
 | 
			
		||||
                error_msg = str(original).split('API Error')[1].strip()
 | 
			
		||||
                await ctx.send(f"Jellyseerr API error: {error_msg}")
 | 
			
		||||
            except:
 | 
			
		||||
                await ctx.send(f"Jellyseerr API error: {str(original)}")
 | 
			
		||||
        elif "Connection error" in str(original):
 | 
			
		||||
            await ctx.send(f"Connection error: Cannot connect to Jellyseerr. Please check if the server is running and try again later.")
 | 
			
		||||
        elif "timeout" in str(original).lower():
 | 
			
		||||
            await ctx.send(f"The request timed out. The Jellyseerr server might be slow or unreachable. Please try again later.")
 | 
			
		||||
        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():
 | 
			
		||||
    """Main function to initialize and start the bot."""
 | 
			
		||||
    try:
 | 
			
		||||
        async with bot:
 | 
			
		||||
            logger.info("Starting Discord Jellyseerr Bot")
 | 
			
		||||
            logger.info(f"Bot version: 1.0.0")
 | 
			
		||||
            logger.info(f"Python version: {sys.version}")
 | 
			
		||||
            logger.info(f"Discord.py version: {discord.__version__}")
 | 
			
		||||
            
 | 
			
		||||
            if config.JELLYSEERR_LOCAL_LOGIN:
 | 
			
		||||
                logger.info(f"Using Jellyseerr authentication with account: {config.JELLYSEERR_EMAIL}")
 | 
			
		||||
            else:
 | 
			
		||||
@@ -193,9 +250,13 @@ async def main():
 | 
			
		||||
                logger.debug("DEBUG MODE ENABLED - verbose logging activated")
 | 
			
		||||
                
 | 
			
		||||
            await load_extensions()
 | 
			
		||||
            logger.info("Connecting to Discord...")
 | 
			
		||||
            await bot.start(config.BOT_TOKEN)
 | 
			
		||||
    except KeyboardInterrupt:
 | 
			
		||||
        logger.info("Bot shutting down by keyboard interrupt")
 | 
			
		||||
    except discord.LoginFailure:
 | 
			
		||||
        logger.critical("Invalid Discord token. Please check your DISCORD_BOT_TOKEN in the .env file.", exc_info=True)
 | 
			
		||||
        sys.exit(1)
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.critical(f"Fatal error: {str(e)}", exc_info=True)
 | 
			
		||||
        sys.exit(1)
 | 
			
		||||
@@ -203,6 +264,9 @@ async def main():
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    try:
 | 
			
		||||
        asyncio.run(main())
 | 
			
		||||
    except KeyboardInterrupt:
 | 
			
		||||
        print("Bot shut down by keyboard interrupt")
 | 
			
		||||
        sys.exit(0)
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        print(f"Fatal error occurred: {str(e)}")
 | 
			
		||||
        logging.critical(f"Fatal error in main process: {str(e)}", exc_info=True)
 | 
			
		||||
 
 | 
			
		||||
@@ -11,14 +11,26 @@ 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"""
 | 
			
		||||
        """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:
 | 
			
		||||
@@ -93,26 +105,34 @@ class UtilityCommands(commands.Cog):
 | 
			
		||||
    
 | 
			
		||||
    @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
 | 
			
		||||
        """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 media_id:
 | 
			
		||||
        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}...")
 | 
			
		||||
        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`")
 | 
			
		||||
        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():
 | 
			
		||||
@@ -128,17 +148,44 @@ class UtilityCommands(commands.Cog):
 | 
			
		||||
                
 | 
			
		||||
                # 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
 | 
			
		||||
                    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}?api_key=12345") as tv_response:
 | 
			
		||||
                        tv_status = tv_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
 | 
			
		||||
                    is_movie = movie_status == 401  # 401 means it exists but auth failed
 | 
			
		||||
                    is_tv = tv_status == 401  # 401 means it exists but auth failed
 | 
			
		||||
                        # 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(
 | 
			
		||||
@@ -308,7 +355,14 @@ class UtilityCommands(commands.Cog):
 | 
			
		||||
    
 | 
			
		||||
    @commands.command(name="status")
 | 
			
		||||
    async def get_status(self, ctx):
 | 
			
		||||
        """Check Jellyseerr server status"""
 | 
			
		||||
        """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()
 | 
			
		||||
@@ -344,5 +398,12 @@ class UtilityCommands(commands.Cog):
 | 
			
		||||
                
 | 
			
		||||
                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)}")
 | 
			
		||||
		Reference in New Issue
	
	Block a user