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)