Code improvements
This commit is contained in:
parent
4281aafc61
commit
fe567952b4
52
.env.example
52
.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
|
||||
REQUEST_TIMEOUT=30
|
||||
# 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
|
96
commands.py
96
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
|
||||
@ -219,13 +251,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 +325,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 +387,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
|
||||
|
||||
@ -481,7 +549,13 @@ class RequestCommands(commands.Cog):
|
||||
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 +645,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 +660,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,
|
||||
|
92
main.py
92
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)
|
||||
|
||||
# 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")
|
||||
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
|
||||
|
||||
# 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}')
|
||||
|
||||
@ -132,6 +155,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):
|
||||
@ -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}", timeout=aiohttp.ClientTimeout(total=15)) as tv_response:
|
||||
tv_status = tv_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
|
||||
# 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)}")
|
Loading…
x
Reference in New Issue
Block a user