Code improvements
This commit is contained in:
parent
4281aafc61
commit
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
|
# 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
|
DISCORD_BOT_TOKEN=your_discord_bot_token_here
|
||||||
|
|
||||||
# Bot command prefix (default is !)
|
# Command prefix for bot commands (default: !)
|
||||||
BOT_PREFIX=!
|
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_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_EMAIL=your_jellyseerr_email@example.com
|
||||||
JELLYSEERR_PASSWORD=your_jellyseerr_password
|
JELLYSEERR_PASSWORD=your_jellyseerr_password
|
||||||
|
|
||||||
# Set to false if you want to disable local login (not recommended)
|
# Set to false if you want to disable local login (not recommended)
|
||||||
|
# When disabled, some functionality may be limited
|
||||||
JELLYSEERR_LOCAL_LOGIN=true
|
JELLYSEERR_LOCAL_LOGIN=true
|
||||||
|
|
||||||
# Number of days until authentication cookie expires (default: 7)
|
# Number of days until authentication cookie expires (default: 7)
|
||||||
AUTH_COOKIE_EXPIRY=7
|
AUTH_COOKIE_EXPIRY=7
|
||||||
|
|
||||||
|
#---------------------------------------------------------------------
|
||||||
# Notifications
|
# Notifications
|
||||||
# Jellyseerr provides webhooks for notifications, which can be configured
|
#---------------------------------------------------------------------
|
||||||
# directly in the Jellyseerr settings. Discord webhooks are recommended
|
# NOTE: For notifications about media requests and availability,
|
||||||
# instead of using this bot for notifications.
|
# use Jellyseerr's built-in Discord webhook integration.
|
||||||
|
# Configure webhooks directly in the Jellyseerr web interface
|
||||||
# UI Settings
|
# under Settings > Notifications.
|
||||||
EMBED_COLOR=0x3498db
|
|
||||||
|
|
||||||
# Debug Settings
|
|
||||||
# Set to true to enable verbose logging (useful for troubleshooting)
|
|
||||||
DEBUG_MODE=false
|
|
||||||
|
|
||||||
|
#---------------------------------------------------------------------
|
||||||
|
# Performance & Debug Settings
|
||||||
|
#---------------------------------------------------------------------
|
||||||
# API request timeout in seconds
|
# API request timeout in seconds
|
||||||
|
# Increase this value if your Jellyseerr instance is slow to respond
|
||||||
REQUEST_TIMEOUT=30
|
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
|
94
commands.py
94
commands.py
@ -16,6 +16,17 @@ import sys
|
|||||||
jellyseerr_api = None
|
jellyseerr_api = None
|
||||||
|
|
||||||
def get_api():
|
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
|
global jellyseerr_api
|
||||||
if jellyseerr_api is None:
|
if jellyseerr_api is None:
|
||||||
# Get the API client instance from the main module
|
# Get the API client instance from the main module
|
||||||
@ -25,10 +36,19 @@ def get_api():
|
|||||||
else:
|
else:
|
||||||
# This should not happen, but just in case
|
# This should not happen, but just in case
|
||||||
logger.error("Could not find jellyseerr_api instance")
|
logger.error("Could not find jellyseerr_api instance")
|
||||||
|
raise RuntimeError("API client not initialized. This is likely a bug.")
|
||||||
return jellyseerr_api
|
return jellyseerr_api
|
||||||
|
|
||||||
def safe_int_convert(value, default=None):
|
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:
|
if value is None:
|
||||||
return default
|
return default
|
||||||
try:
|
try:
|
||||||
@ -37,13 +57,25 @@ def safe_int_convert(value, default=None):
|
|||||||
return default
|
return default
|
||||||
|
|
||||||
class MediaCommands(commands.Cog):
|
class MediaCommands(commands.Cog):
|
||||||
|
"""Commands for searching and displaying media information."""
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
|
"""Initialize the MediaCommands cog.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot: The Discord bot instance
|
||||||
|
"""
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.embed_color = config.EMBED_COLOR
|
self.embed_color = config.EMBED_COLOR
|
||||||
|
|
||||||
@commands.command(name="info", aliases=["i"])
|
@commands.command(name="info", aliases=["i"])
|
||||||
async def get_media_info(self, ctx, media_id = None):
|
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:
|
if not media_id:
|
||||||
await ctx.send("Please provide a media ID. Example: `!info 550` (for Fight Club)")
|
await ctx.send("Please provide a media ID. Example: `!info 550` (for Fight Club)")
|
||||||
return
|
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"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.")
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Error retrieving media details for ID {media_id}: {str(e)}", exc_info=True)
|
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)}")
|
await ctx.send(f"Error retrieving media details: {str(e)}")
|
||||||
|
|
||||||
@commands.command(name="search", aliases=["s"])
|
@commands.command(name="search", aliases=["s"])
|
||||||
async def search_media(self, ctx, *, query: str = None):
|
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:
|
if not query:
|
||||||
await ctx.send("Please provide a search term. Example: `!search Stranger Things`")
|
await ctx.send("Please provide a search term. Example: `!search Stranger Things`")
|
||||||
return
|
return
|
||||||
@ -282,14 +325,25 @@ class MediaCommands(commands.Cog):
|
|||||||
|
|
||||||
await ctx.send(embed=embed)
|
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:
|
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)}")
|
await ctx.send(f"Error searching for media: {str(e)}")
|
||||||
|
|
||||||
# Movie and TV commands removed - replaced by the unified info command
|
# Movie and TV commands removed - replaced by the unified info command
|
||||||
|
|
||||||
@commands.command(name="trending")
|
@commands.command(name="trending")
|
||||||
async def get_trending(self, ctx):
|
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():
|
async with ctx.typing():
|
||||||
try:
|
try:
|
||||||
api = get_api()
|
api = get_api()
|
||||||
@ -333,11 +387,25 @@ class MediaCommands(commands.Cog):
|
|||||||
|
|
||||||
await ctx.send(embed=embed)
|
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:
|
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)}")
|
await ctx.send(f"Error retrieving trending media: {str(e)}")
|
||||||
|
|
||||||
class RequestCommands(commands.Cog):
|
class RequestCommands(commands.Cog):
|
||||||
|
"""Commands for managing media requests."""
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
|
"""Initialize the RequestCommands cog.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot: The Discord bot instance
|
||||||
|
"""
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.embed_color = config.EMBED_COLOR
|
self.embed_color = config.EMBED_COLOR
|
||||||
|
|
||||||
@ -480,6 +548,12 @@ class RequestCommands(commands.Cog):
|
|||||||
f"Try searching for the media first with `{config.BOT_PREFIX}search` to get a valid ID")
|
f"Try searching for the media first with `{config.BOT_PREFIX}search` to get a valid ID")
|
||||||
elif "401" in error_message or "403" in error_message:
|
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.")
|
await ctx.send(f"Error creating request: Authentication error. The bot account may not have permission to make requests.")
|
||||||
|
else:
|
||||||
|
# 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:
|
else:
|
||||||
await ctx.send(f"Error creating request: {str(e)}")
|
await ctx.send(f"Error creating request: {str(e)}")
|
||||||
|
|
||||||
@ -571,7 +645,14 @@ class RequestCommands(commands.Cog):
|
|||||||
|
|
||||||
await ctx.send(embed=embed)
|
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:
|
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)}")
|
await ctx.send(f"Error retrieving requests: {str(e)}")
|
||||||
|
|
||||||
# Approve and decline commands have been removed
|
# Approve and decline commands have been removed
|
||||||
@ -579,6 +660,11 @@ class RequestCommands(commands.Cog):
|
|||||||
# UtilityCommands have been moved to utility_commands.py
|
# UtilityCommands have been moved to utility_commands.py
|
||||||
|
|
||||||
def setup(bot):
|
def setup(bot):
|
||||||
|
"""Set up the command cogs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot: The Discord bot instance
|
||||||
|
"""
|
||||||
bot.add_cog(MediaCommands(bot))
|
bot.add_cog(MediaCommands(bot))
|
||||||
bot.add_cog(RequestCommands(bot))
|
bot.add_cog(RequestCommands(bot))
|
||||||
bot.add_cog(UtilityCommands(bot))
|
bot.add_cog(UtilityCommands(bot))
|
@ -10,17 +10,29 @@ from typing import Dict, List, Optional, Any, Union
|
|||||||
logger = logging.getLogger('jellyseerr_api')
|
logger = logging.getLogger('jellyseerr_api')
|
||||||
|
|
||||||
class JellyseerrAPI:
|
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):
|
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.base_url = base_url.rstrip('/')
|
||||||
self.email = email
|
self.email = email
|
||||||
self.password = password
|
self.password = password
|
||||||
self.headers = {
|
self.headers = {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
self.session = None
|
self.session: Optional[aiohttp.ClientSession] = None
|
||||||
self.cookie_jar = None
|
self.cookie_jar: Optional[aiohttp.CookieJar] = None
|
||||||
self.auth_cookie = None
|
self.auth_cookie: Optional[str] = None
|
||||||
self.last_login = None
|
self.last_login: Optional[datetime] = None
|
||||||
|
|
||||||
async def __aenter__(self):
|
async def __aenter__(self):
|
||||||
if not self.cookie_jar:
|
if not self.cookie_jar:
|
||||||
@ -34,7 +46,14 @@ class JellyseerrAPI:
|
|||||||
await self.session.close()
|
await self.session.close()
|
||||||
|
|
||||||
async def login(self) -> bool:
|
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
|
# Skip login if we have a valid cookie and it's not expired
|
||||||
cookie_expiry_days = config.AUTH_COOKIE_EXPIRY
|
cookie_expiry_days = config.AUTH_COOKIE_EXPIRY
|
||||||
if (self.auth_cookie and self.last_login and
|
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)
|
logger.error(f"Login error: {str(e)}", exc_info=True)
|
||||||
raise Exception(f"Failed to authenticate with Jellyseerr: {str(e)}")
|
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
|
# Ensure we're logged in
|
||||||
if not self.last_login:
|
if not self.last_login:
|
||||||
try:
|
try:
|
||||||
@ -127,9 +160,8 @@ class JellyseerrAPI:
|
|||||||
|
|
||||||
url = f"{self.base_url}/api/v1{endpoint}"
|
url = f"{self.base_url}/api/v1{endpoint}"
|
||||||
|
|
||||||
# URL encode parameters
|
# URL encode parameters - only encode string values, not numbers or booleans
|
||||||
if params:
|
if params:
|
||||||
# Create a new dict with URL-encoded values
|
|
||||||
encoded_params = {}
|
encoded_params = {}
|
||||||
for key, value in params.items():
|
for key, value in params.items():
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
@ -139,8 +171,17 @@ class JellyseerrAPI:
|
|||||||
params = encoded_params
|
params = encoded_params
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Set timeout for the request
|
||||||
|
timeout = aiohttp.ClientTimeout(total=config.REQUEST_TIMEOUT)
|
||||||
|
|
||||||
# Attempt the request
|
# 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
|
if response.status == 204: # No content
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -153,7 +194,13 @@ class JellyseerrAPI:
|
|||||||
try:
|
try:
|
||||||
await self.login()
|
await self.login()
|
||||||
# Retry the request after re-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}")
|
logger.debug(f"Retry response status: {retry_response.status}")
|
||||||
|
|
||||||
if retry_response.status == 204:
|
if retry_response.status == 204:
|
||||||
@ -180,15 +227,34 @@ class JellyseerrAPI:
|
|||||||
raise Exception(f"API Error ({response.status}): {error_message}")
|
raise Exception(f"API Error ({response.status}): {error_message}")
|
||||||
|
|
||||||
return response_data
|
return response_data
|
||||||
except aiohttp.ClientResponseError:
|
except aiohttp.ContentTypeError:
|
||||||
raise Exception(f"API Error ({response.status}): Failed to parse response")
|
# Not JSON, get the text response
|
||||||
except aiohttp.ClientError as e:
|
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)}")
|
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)}")
|
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
|
# Search endpoints
|
||||||
async def search(self, query: str, page: int = 1, language: str = 'en') -> Dict:
|
async def search(self, query: str, page: int = 1, language: str = 'en') -> Dict[str, Any]:
|
||||||
"""Search for movies, TV shows, or people"""
|
"""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 = {
|
params = {
|
||||||
'query': query,
|
'query': query,
|
||||||
'page': page,
|
'page': page,
|
||||||
@ -197,32 +263,75 @@ class JellyseerrAPI:
|
|||||||
return await self._request('GET', '/search', params=params)
|
return await self._request('GET', '/search', params=params)
|
||||||
|
|
||||||
# Media information endpoints
|
# Media information endpoints
|
||||||
async def get_movie_details(self, movie_id: int, language: str = 'en') -> Dict:
|
async def get_movie_details(self, movie_id: int, language: str = 'en') -> Dict[str, Any]:
|
||||||
"""Get detailed information about a movie"""
|
"""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}
|
params = {'language': language}
|
||||||
return await self._request('GET', f'/movie/{movie_id}', params=params)
|
return await self._request('GET', f'/movie/{movie_id}', params=params)
|
||||||
|
|
||||||
async def get_tv_details(self, tv_id: int, language: str = 'en') -> Dict:
|
async def get_tv_details(self, tv_id: int, language: str = 'en') -> Dict[str, Any]:
|
||||||
"""Get detailed information about a TV show"""
|
"""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}
|
params = {'language': language}
|
||||||
return await self._request('GET', f'/tv/{tv_id}', params=params)
|
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:
|
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"""
|
"""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}
|
params = {'language': language}
|
||||||
return await self._request('GET', f'/tv/{tv_id}/season/{season_id}', params=params)
|
return await self._request('GET', f'/tv/{tv_id}/season/{season_id}', params=params)
|
||||||
|
|
||||||
# Recommendation endpoints
|
# Recommendation endpoints
|
||||||
async def get_movie_recommendations(self, movie_id: int, page: int = 1, language: str = 'en') -> Dict:
|
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"""
|
"""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 = {
|
params = {
|
||||||
'page': page,
|
'page': page,
|
||||||
'language': language
|
'language': language
|
||||||
}
|
}
|
||||||
return await self._request('GET', f'/movie/{movie_id}/recommendations', params=params)
|
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:
|
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"""
|
"""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 = {
|
params = {
|
||||||
'page': page,
|
'page': page,
|
||||||
'language': language
|
'language': language
|
||||||
@ -241,8 +350,21 @@ class JellyseerrAPI:
|
|||||||
|
|
||||||
async def create_request(self, media_type: str, media_id: int,
|
async def create_request(self, media_type: str, media_id: int,
|
||||||
seasons: Union[List[int], str] = None,
|
seasons: Union[List[int], str] = None,
|
||||||
is_4k: bool = False) -> Dict:
|
is_4k: bool = False) -> Dict[str, Any]:
|
||||||
"""Create a new media request"""
|
"""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 = {
|
data = {
|
||||||
'mediaType': media_type,
|
'mediaType': media_type,
|
||||||
'mediaId': media_id,
|
'mediaId': media_id,
|
||||||
|
72
main.py
72
main.py
@ -10,9 +10,14 @@ import traceback
|
|||||||
|
|
||||||
# Set up logging
|
# Set up logging
|
||||||
def setup_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'
|
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'
|
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
|
# Create logs directory if it doesn't exist
|
||||||
os.makedirs('logs', exist_ok=True)
|
os.makedirs('logs', exist_ok=True)
|
||||||
@ -21,9 +26,14 @@ def setup_logging():
|
|||||||
root_logger = logging.getLogger()
|
root_logger = logging.getLogger()
|
||||||
root_logger.setLevel(log_level)
|
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
|
||||||
console_handler = logging.StreamHandler(sys.stdout)
|
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)
|
console_handler.setLevel(log_level)
|
||||||
|
|
||||||
# File handler with rotation (5 files, 5MB each)
|
# File handler with rotation (5 files, 5MB each)
|
||||||
@ -74,6 +84,7 @@ jellyseerr_api = None
|
|||||||
|
|
||||||
# Load all cogs
|
# Load all cogs
|
||||||
async def load_extensions():
|
async def load_extensions():
|
||||||
|
"""Load all command extensions and initialize the API client."""
|
||||||
# Import here to avoid circular imports
|
# Import here to avoid circular imports
|
||||||
from commands import MediaCommands, RequestCommands
|
from commands import MediaCommands, RequestCommands
|
||||||
from utility_commands import UtilityCommands
|
from utility_commands import UtilityCommands
|
||||||
@ -81,23 +92,35 @@ async def load_extensions():
|
|||||||
|
|
||||||
# Initialize the API client
|
# Initialize the API client
|
||||||
global jellyseerr_api
|
global jellyseerr_api
|
||||||
|
try:
|
||||||
jellyseerr_api = JellyseerrAPI(config.JELLYSEERR_URL, config.JELLYSEERR_EMAIL, config.JELLYSEERR_PASSWORD)
|
jellyseerr_api = JellyseerrAPI(config.JELLYSEERR_URL, config.JELLYSEERR_EMAIL, config.JELLYSEERR_PASSWORD)
|
||||||
|
|
||||||
# Make API client available to command modules
|
# Make API client available to command modules
|
||||||
import sys
|
import sys
|
||||||
sys.modules['jellyseerr_api'].jellyseerr_api = jellyseerr_api
|
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(MediaCommands(bot))
|
||||||
await bot.add_cog(RequestCommands(bot))
|
await bot.add_cog(RequestCommands(bot))
|
||||||
await bot.add_cog(UtilityCommands(bot))
|
await bot.add_cog(UtilityCommands(bot))
|
||||||
logger.info("Loaded all extensions")
|
logger.info("Loaded all extensions")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to initialize extensions: {str(e)}", exc_info=True)
|
||||||
|
raise
|
||||||
|
|
||||||
@bot.event
|
@bot.event
|
||||||
async def on_ready():
|
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'Logged in as {bot.user} (ID: {bot.user.id})')
|
||||||
logger.info(f'Connected to {len(bot.guilds)} guilds')
|
logger.info(f'Connected to {len(bot.guilds)} guilds')
|
||||||
|
|
||||||
|
# List connected guilds
|
||||||
|
guild_list = []
|
||||||
for guild in bot.guilds:
|
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' - {guild.name} (ID: {guild.id})')
|
||||||
|
|
||||||
logger.info(f'Using prefix: {config.BOT_PREFIX}')
|
logger.info(f'Using prefix: {config.BOT_PREFIX}')
|
||||||
logger.info(f'Jellyseerr URL: {config.JELLYSEERR_URL}')
|
logger.info(f'Jellyseerr URL: {config.JELLYSEERR_URL}')
|
||||||
|
|
||||||
@ -133,6 +156,20 @@ async def on_ready():
|
|||||||
await bot.change_presence(activity=activity)
|
await bot.change_presence(activity=activity)
|
||||||
logger.info("Bot is ready and fully operational")
|
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
|
@bot.event
|
||||||
async def on_command(ctx):
|
async def on_command(ctx):
|
||||||
logger.info(f"Command '{ctx.command}' invoked by {ctx.author} in {ctx.guild}/{ctx.channel}")
|
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
|
@bot.event
|
||||||
async def on_command_error(ctx, error):
|
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):
|
if isinstance(error, commands.CommandNotFound):
|
||||||
logger.debug(f"Command not found: {ctx.message.content}")
|
logger.debug(f"Command not found: {ctx.message.content}")
|
||||||
return
|
return
|
||||||
@ -169,21 +212,35 @@ async def on_command_error(ctx, error):
|
|||||||
original = error.original if hasattr(error, 'original') else error
|
original = error.original if hasattr(error, 'original') else error
|
||||||
|
|
||||||
# Create a detailed error message for logging
|
# 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)
|
logger.error(error_details, exc_info=True)
|
||||||
|
|
||||||
# Create a user-friendly error message
|
# Create a user-friendly error message
|
||||||
if "400" in str(original) and "must be url encoded" in str(original).lower():
|
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.")
|
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):
|
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:
|
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}`.")
|
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():
|
async def main():
|
||||||
|
"""Main function to initialize and start the bot."""
|
||||||
try:
|
try:
|
||||||
async with bot:
|
async with bot:
|
||||||
logger.info("Starting Discord Jellyseerr 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:
|
if config.JELLYSEERR_LOCAL_LOGIN:
|
||||||
logger.info(f"Using Jellyseerr authentication with account: {config.JELLYSEERR_EMAIL}")
|
logger.info(f"Using Jellyseerr authentication with account: {config.JELLYSEERR_EMAIL}")
|
||||||
else:
|
else:
|
||||||
@ -193,9 +250,13 @@ async def main():
|
|||||||
logger.debug("DEBUG MODE ENABLED - verbose logging activated")
|
logger.debug("DEBUG MODE ENABLED - verbose logging activated")
|
||||||
|
|
||||||
await load_extensions()
|
await load_extensions()
|
||||||
|
logger.info("Connecting to Discord...")
|
||||||
await bot.start(config.BOT_TOKEN)
|
await bot.start(config.BOT_TOKEN)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logger.info("Bot shutting down by keyboard interrupt")
|
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:
|
except Exception as e:
|
||||||
logger.critical(f"Fatal error: {str(e)}", exc_info=True)
|
logger.critical(f"Fatal error: {str(e)}", exc_info=True)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@ -203,6 +264,9 @@ async def main():
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
try:
|
try:
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Bot shut down by keyboard interrupt")
|
||||||
|
sys.exit(0)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Fatal error occurred: {str(e)}")
|
print(f"Fatal error occurred: {str(e)}")
|
||||||
logging.critical(f"Fatal error in main process: {str(e)}", exc_info=True)
|
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')
|
logger = logging.getLogger('utility_commands')
|
||||||
|
|
||||||
class UtilityCommands(commands.Cog):
|
class UtilityCommands(commands.Cog):
|
||||||
|
"""Commands for utility functions like help, status, and testing."""
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
|
"""Initialize the UtilityCommands cog.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot: The Discord bot instance
|
||||||
|
"""
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.embed_color = config.EMBED_COLOR
|
self.embed_color = config.EMBED_COLOR
|
||||||
self.tmdb_base_url = "https://api.themoviedb.org/3"
|
self.tmdb_base_url = "https://api.themoviedb.org/3"
|
||||||
|
|
||||||
@commands.command(name="help", aliases=["h"])
|
@commands.command(name="help", aliases=["h"])
|
||||||
async def custom_help(self, ctx, command: str = None):
|
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:
|
if command:
|
||||||
cmd = self.bot.get_command(command)
|
cmd = self.bot.get_command(command)
|
||||||
if cmd:
|
if cmd:
|
||||||
@ -93,26 +105,34 @@ class UtilityCommands(commands.Cog):
|
|||||||
|
|
||||||
@commands.command(name="testmedia")
|
@commands.command(name="testmedia")
|
||||||
async def test_media(self, ctx, media_type: str = None, media_id = None):
|
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
|
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
|
# If only one parameter is provided, assume it's the ID
|
||||||
media_id = media_type
|
media_id = media_type
|
||||||
media_type = None
|
media_type = None
|
||||||
await ctx.send(f"Testing both movie and TV show for ID {media_id}...")
|
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']:
|
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.")
|
await ctx.send("Invalid media type. Use 'movie' or 'tv' or just provide the ID to test both.")
|
||||||
return
|
return
|
||||||
|
|
||||||
media_id = safe_int_convert(media_id)
|
media_id = safe_int_convert(media_id)
|
||||||
if not media_id:
|
if not media_id or media_id <= 0:
|
||||||
await ctx.send(f"Please provide a valid media ID. Example: `!testmedia 550`")
|
await ctx.send(f"Please provide a valid media ID (a positive number). Example: `!testmedia 550`")
|
||||||
return
|
return
|
||||||
|
|
||||||
async with ctx.typing():
|
async with ctx.typing():
|
||||||
@ -128,17 +148,44 @@ class UtilityCommands(commands.Cog):
|
|||||||
|
|
||||||
# Test both movie and TV endpoints directly using HTTP requests
|
# Test both movie and TV endpoints directly using HTTP requests
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
|
try:
|
||||||
# Test movie endpoint
|
# Test movie endpoint
|
||||||
async with session.get(f"{self.tmdb_base_url}/movie/{media_id}?api_key=12345") as movie_response:
|
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
|
movie_status = movie_response.status
|
||||||
|
|
||||||
# Test TV endpoint
|
# Test TV endpoint
|
||||||
async with session.get(f"{self.tmdb_base_url}/tv/{media_id}?api_key=12345") as tv_response:
|
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
|
tv_status = tv_response.status
|
||||||
|
|
||||||
# Check which type of media this ID belongs to
|
# Check which type of media this ID belongs to
|
||||||
is_movie = movie_status == 401 # 401 means it exists but auth failed
|
# 401 or 404 with specific message means it exists but auth failed
|
||||||
is_tv = tv_status == 401 # 401 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:
|
if is_movie and not is_tv:
|
||||||
embed.add_field(
|
embed.add_field(
|
||||||
@ -308,7 +355,14 @@ class UtilityCommands(commands.Cog):
|
|||||||
|
|
||||||
@commands.command(name="status")
|
@commands.command(name="status")
|
||||||
async def get_status(self, ctx):
|
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():
|
async with ctx.typing():
|
||||||
try:
|
try:
|
||||||
api = get_api()
|
api = get_api()
|
||||||
@ -344,5 +398,12 @@ class UtilityCommands(commands.Cog):
|
|||||||
|
|
||||||
await ctx.send(embed=embed)
|
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:
|
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)}")
|
await ctx.send(f"Error checking server status: {str(e)}")
|
Loading…
x
Reference in New Issue
Block a user