750 lines
36 KiB
Python
750 lines
36 KiB
Python
import discord
|
|
from discord.ext import commands
|
|
import asyncio
|
|
import aiohttp
|
|
from typing import Optional, List, Dict, Union, Any
|
|
import re
|
|
import logging
|
|
|
|
import config
|
|
# We'll import jellyseerr_api later to avoid circular imports
|
|
|
|
logger = logging.getLogger('commands')
|
|
|
|
# Get the API client from main module (to avoid circular imports)
|
|
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
|
|
if 'jellyseerr_api' in sys.modules and hasattr(sys.modules['jellyseerr_api'], 'jellyseerr_api'):
|
|
jellyseerr_api = sys.modules['jellyseerr_api'].jellyseerr_api
|
|
logger.debug("Retrieved jellyseerr_api instance from module")
|
|
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.
|
|
|
|
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:
|
|
return int(value)
|
|
except (ValueError, TypeError):
|
|
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).
|
|
|
|
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
|
|
|
|
media_id = safe_int_convert(media_id)
|
|
if not media_id or media_id <= 0:
|
|
await ctx.send("Media ID must be a positive number.")
|
|
return
|
|
|
|
async with ctx.typing():
|
|
try:
|
|
api = get_api()
|
|
logger.info(f"Looking up media with ID: {media_id}")
|
|
|
|
# Try to get as movie first
|
|
try:
|
|
movie = await api.get_movie_details(media_id)
|
|
if movie and movie.get('title'):
|
|
# It's a movie, show movie details
|
|
embed = discord.Embed(
|
|
title=f"{movie.get('title')} ({movie.get('releaseDate', 'Unknown')[:4]})",
|
|
description=movie.get('overview'),
|
|
color=self.embed_color,
|
|
url=f"{config.JELLYSEERR_URL}/movie/{media_id}"
|
|
)
|
|
|
|
# Add poster if available
|
|
if movie.get('posterPath'):
|
|
embed.set_thumbnail(url=f"https://image.tmdb.org/t/p/w500{movie.get('posterPath')}")
|
|
|
|
# Add rating and runtime
|
|
rating_value = movie.get('voteAverage', 0)
|
|
rating_str = f"⭐ {rating_value}/10" if rating_value else "Not rated"
|
|
|
|
runtime = movie.get('runtime', 0)
|
|
runtime_str = f"{runtime} mins" if runtime else "Unknown"
|
|
|
|
embed.add_field(name="Type", value="Movie", inline=True)
|
|
embed.add_field(name="Rating", value=rating_str, inline=True)
|
|
embed.add_field(name="Runtime", value=runtime_str, inline=True)
|
|
|
|
# Add genres
|
|
genres = movie.get('genres', [])
|
|
if genres:
|
|
genres_str = ", ".join([genre.get('name') for genre in genres])
|
|
embed.add_field(name="Genres", value=genres_str, inline=True)
|
|
|
|
# Add media status if available
|
|
media_info = movie.get('mediaInfo')
|
|
if media_info:
|
|
status = media_info.get('status')
|
|
status_map = {
|
|
1: "Unknown",
|
|
2: "Pending",
|
|
3: "Processing",
|
|
4: "Partially Available",
|
|
5: "Available"
|
|
}
|
|
status_str = status_map.get(status, "Unknown")
|
|
embed.add_field(name="Status", value=status_str, inline=True)
|
|
|
|
# Add request button info or status
|
|
if not media_info:
|
|
embed.add_field(
|
|
name="Request this Movie",
|
|
value=f"Use `{config.BOT_PREFIX}request movie {media_id}` to request this movie",
|
|
inline=False
|
|
)
|
|
elif media_info.get('status') == 5: # Available
|
|
embed.add_field(
|
|
name="Status",
|
|
value="✅ This movie is available in your library",
|
|
inline=False
|
|
)
|
|
elif media_info.get('status') == 3: # Processing
|
|
embed.add_field(
|
|
name="Status",
|
|
value="⏳ This movie is being processed",
|
|
inline=False
|
|
)
|
|
elif media_info.get('status') == 2: # Pending
|
|
embed.add_field(
|
|
name="Status",
|
|
value="⏳ This movie has been requested and is pending",
|
|
inline=False
|
|
)
|
|
elif media_info.get('status') < 5:
|
|
embed.add_field(
|
|
name="Request this Movie",
|
|
value=f"Use `{config.BOT_PREFIX}request movie {media_id}` to request this movie",
|
|
inline=False
|
|
)
|
|
|
|
await ctx.send(embed=embed)
|
|
return
|
|
except Exception as movie_error:
|
|
# Not a movie, try as TV show
|
|
pass
|
|
|
|
# Try as TV show
|
|
try:
|
|
tv = await api.get_tv_details(media_id)
|
|
if tv and tv.get('name'):
|
|
# It's a TV show, show TV details
|
|
title = tv.get('name')
|
|
first_air_date = tv.get('firstAirDate', '')
|
|
year = f" ({first_air_date[:4]})" if first_air_date else ""
|
|
|
|
embed = discord.Embed(
|
|
title=f"{title}{year}",
|
|
description=tv.get('overview'),
|
|
color=self.embed_color,
|
|
url=f"{config.JELLYSEERR_URL}/tv/{media_id}"
|
|
)
|
|
|
|
# Add poster if available
|
|
if tv.get('posterPath'):
|
|
embed.set_thumbnail(url=f"https://image.tmdb.org/t/p/w500{tv.get('posterPath')}")
|
|
|
|
# Add rating and seasons info
|
|
rating_value = tv.get('voteAverage', 0)
|
|
rating_str = f"⭐ {rating_value}/10" if rating_value else "Not rated"
|
|
|
|
seasons_count = tv.get('numberOfSeasons', 0)
|
|
episodes_count = tv.get('numberOfEpisodes', 0)
|
|
seasons_str = f"{seasons_count} seasons ({episodes_count} episodes)"
|
|
|
|
embed.add_field(name="Type", value="TV Show", inline=True)
|
|
embed.add_field(name="Rating", value=rating_str, inline=True)
|
|
embed.add_field(name="Seasons", value=seasons_str, inline=True)
|
|
|
|
# Add status
|
|
status = tv.get('status', 'Unknown')
|
|
embed.add_field(name="Status", value=status, inline=True)
|
|
|
|
# Add genres
|
|
genres = tv.get('genres', [])
|
|
if genres:
|
|
genres_str = ", ".join([genre.get('name') for genre in genres])
|
|
embed.add_field(name="Genres", value=genres_str, inline=True)
|
|
|
|
# Add networks
|
|
networks = tv.get('networks', [])
|
|
if networks:
|
|
networks_str = ", ".join([network.get('name') for network in networks])
|
|
embed.add_field(name="Networks", value=networks_str, inline=True)
|
|
|
|
# Add media status if available
|
|
media_info = tv.get('mediaInfo')
|
|
if media_info:
|
|
status = media_info.get('status')
|
|
status_map = {
|
|
1: "Unknown",
|
|
2: "Pending",
|
|
3: "Processing",
|
|
4: "Partially Available",
|
|
5: "Available"
|
|
}
|
|
status_str = status_map.get(status, "Unknown")
|
|
embed.add_field(name="Availability", value=status_str, inline=True)
|
|
|
|
# Add seasons list (compact)
|
|
seasons = tv.get('seasons', [])
|
|
if seasons:
|
|
valid_seasons = [
|
|
season for season in seasons
|
|
if season.get('seasonNumber', 0) > 0
|
|
]
|
|
|
|
if valid_seasons:
|
|
season_nums = [str(season.get('seasonNumber')) for season in valid_seasons]
|
|
season_list = ", ".join(season_nums)
|
|
embed.add_field(
|
|
name=f"Available Seasons",
|
|
value=season_list,
|
|
inline=False
|
|
)
|
|
|
|
# Add request button info or status
|
|
if not media_info:
|
|
embed.add_field(
|
|
name="Request this Show",
|
|
value=f"Use `{config.BOT_PREFIX}request tv {media_id}` to request all seasons or\n"
|
|
f"`{config.BOT_PREFIX}request tv {media_id} <season_nums>` for specific seasons",
|
|
inline=False
|
|
)
|
|
elif media_info.get('status') == 5: # Available
|
|
embed.add_field(
|
|
name="Status",
|
|
value="✅ This TV show is available in your library",
|
|
inline=False
|
|
)
|
|
elif media_info.get('status') == 4: # Partially Available
|
|
embed.add_field(
|
|
name="Status",
|
|
value="⚠️ This TV show is partially available. You may want to request specific seasons.",
|
|
inline=False
|
|
)
|
|
embed.add_field(
|
|
name="Request Specific Seasons",
|
|
value=f"Use `{config.BOT_PREFIX}request tv {media_id} <season_nums>` to request specific seasons",
|
|
inline=False
|
|
)
|
|
elif media_info.get('status') == 3: # Processing
|
|
embed.add_field(
|
|
name="Status",
|
|
value="⏳ This TV show is being processed",
|
|
inline=False
|
|
)
|
|
elif media_info.get('status') == 2: # Pending
|
|
embed.add_field(
|
|
name="Status",
|
|
value="⏳ This TV show has been requested and is pending",
|
|
inline=False
|
|
)
|
|
elif media_info.get('status') < 5:
|
|
embed.add_field(
|
|
name="Request this Show",
|
|
value=f"Use `{config.BOT_PREFIX}request tv {media_id}` to request all seasons or\n"
|
|
f"`{config.BOT_PREFIX}request tv {media_id} <season_nums>` for specific seasons",
|
|
inline=False
|
|
)
|
|
|
|
await ctx.send(embed=embed)
|
|
return
|
|
except Exception as tv_error:
|
|
# Not a TV show either
|
|
pass
|
|
|
|
# If we get here, the ID doesn't exist as either movie or TV show
|
|
await ctx.send(f"Error: Could not find media with ID {media_id}. This could be because:\n"
|
|
f"1. The ID doesn't exist in TMDB\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.")
|
|
|
|
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.
|
|
|
|
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
|
|
|
|
if len(query) < 2:
|
|
await ctx.send("Search term must be at least 2 characters long.")
|
|
return
|
|
|
|
async with ctx.typing():
|
|
try:
|
|
api = get_api()
|
|
results = await api.search(query=query)
|
|
|
|
if not results or not results.get('results'):
|
|
await ctx.send(f"No results found for '{query}'")
|
|
return
|
|
|
|
# Filter out person results
|
|
media_results = [result for result in results.get('results', [])
|
|
if result.get('mediaType') in ['movie', 'tv']]
|
|
|
|
if not media_results:
|
|
await ctx.send(f"No movie or TV show results found for '{query}'")
|
|
return
|
|
|
|
# Limit to first 5 results for cleaner output
|
|
displayed_results = media_results[:5]
|
|
|
|
embed = discord.Embed(
|
|
title=f"Search Results for '{query}'",
|
|
color=self.embed_color
|
|
)
|
|
|
|
for i, result in enumerate(displayed_results, 1):
|
|
media_type = result.get('mediaType')
|
|
title = result.get('title') if media_type == 'movie' else result.get('name')
|
|
year = ""
|
|
|
|
if media_type == 'movie' and result.get('releaseDate'):
|
|
year = f" ({result.get('releaseDate')[:4]})"
|
|
elif media_type == 'tv' and result.get('firstAirDate'):
|
|
year = f" ({result.get('firstAirDate')[:4]})"
|
|
|
|
media_id = result.get('id')
|
|
|
|
embed.add_field(
|
|
name=f"{i}. {title}{year} ({media_type.upper()})",
|
|
value=f"ID: {media_id}\n{result.get('overview', '')[:100]}...",
|
|
inline=False
|
|
)
|
|
|
|
if len(media_results) > 5:
|
|
embed.set_footer(text=f"Showing 5 out of {len(media_results)} results")
|
|
|
|
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.
|
|
|
|
Args:
|
|
ctx: The command context
|
|
"""
|
|
async with ctx.typing():
|
|
try:
|
|
api = get_api()
|
|
trending = await api.discover_trending()
|
|
|
|
if not trending or not trending.get('results'):
|
|
await ctx.send("No trending media found")
|
|
return
|
|
|
|
# Get first 5 results
|
|
results = trending.get('results', [])[:5]
|
|
|
|
embed = discord.Embed(
|
|
title=f"Trending Movies & TV Shows",
|
|
color=self.embed_color
|
|
)
|
|
|
|
for result in results:
|
|
media_type = result.get('mediaType')
|
|
title = result.get('title') if media_type == 'movie' else result.get('name')
|
|
|
|
year = ""
|
|
if media_type == 'movie' and result.get('releaseDate'):
|
|
year = f" ({result.get('releaseDate')[:4]})"
|
|
elif media_type == 'tv' and result.get('firstAirDate'):
|
|
year = f" ({result.get('firstAirDate')[:4]})"
|
|
|
|
media_id = result.get('id')
|
|
overview = result.get('overview', '')
|
|
if len(overview) > 100:
|
|
overview = f"{overview[:100]}..."
|
|
|
|
embed.add_field(
|
|
name=f"{title}{year} ({media_type.upper()})",
|
|
value=f"ID: {media_id}\n{overview}",
|
|
inline=False
|
|
)
|
|
|
|
# Add note about the new info command
|
|
embed.set_footer(text=f"Tip: Use {config.BOT_PREFIX}info <id> to get details for any result")
|
|
|
|
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
|
|
|
|
@commands.command(name="request", aliases=["req"])
|
|
async def create_request(self, ctx, media_type: str = None, media_id = None, *args):
|
|
"""
|
|
Request a movie or TV show
|
|
Examples:
|
|
!request movie 123
|
|
!request tv 456
|
|
!request tv 456 1,2,3 (for specific seasons)
|
|
|
|
Note: Requests will need to be approved in the Jellyseerr web interface
|
|
"""
|
|
if not media_type:
|
|
await ctx.send("Please specify a media type (`movie` or `tv`). Example: `!request movie 550`")
|
|
return
|
|
|
|
if media_type.lower() not in ['movie', 'tv']:
|
|
await ctx.send("Invalid media type. Use 'movie' or 'tv'.")
|
|
return
|
|
|
|
media_id = safe_int_convert(media_id)
|
|
if not media_id:
|
|
await ctx.send(f"Please provide a valid media ID. Example: `!request {media_type} 550`")
|
|
return
|
|
|
|
if media_id <= 0:
|
|
await ctx.send("Media ID must be a positive number.")
|
|
return
|
|
|
|
async with ctx.typing():
|
|
try:
|
|
# Check if there are specific seasons for TV shows
|
|
seasons = None
|
|
if media_type.lower() == 'tv' and args:
|
|
season_arg = args[0]
|
|
if season_arg.lower() == 'all':
|
|
seasons = 'all'
|
|
else:
|
|
# Parse comma-separated season numbers
|
|
try:
|
|
# Check for valid format
|
|
if not re.match(r'^[0-9]+(,[0-9]+)*$', season_arg):
|
|
await ctx.send("Invalid season format. Use comma-separated numbers (e.g., `1,2,3`) or 'all'.")
|
|
return
|
|
|
|
seasons = [int(s.strip()) for s in season_arg.split(',')]
|
|
|
|
# Check for invalid season numbers
|
|
if any(s <= 0 for s in seasons):
|
|
await ctx.send("Season numbers must be positive integers.")
|
|
return
|
|
except ValueError:
|
|
await ctx.send("Invalid season format. Use comma-separated numbers or 'all'.")
|
|
return
|
|
|
|
# Get media details first to show what's being requested
|
|
api = get_api()
|
|
try:
|
|
if media_type.lower() == 'movie':
|
|
logger.info(f"Getting movie details for request: {media_id}")
|
|
media_details = await api.get_movie_details(media_id)
|
|
title = media_details.get('title')
|
|
year = media_details.get('releaseDate', '')[:4] if media_details.get('releaseDate') else ''
|
|
else: # TV show
|
|
logger.info(f"Getting TV details for request: {media_id}")
|
|
media_details = await api.get_tv_details(media_id)
|
|
title = media_details.get('name')
|
|
year = media_details.get('firstAirDate', '')[:4] if media_details.get('firstAirDate') else ''
|
|
|
|
if not title:
|
|
await ctx.send(f"Could not find {media_type} with ID {media_id}. Please check the ID and try again.")
|
|
return
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error retrieving media details for request: {str(e)}")
|
|
await ctx.send(f"Could not retrieve details for {media_type} with ID {media_id}. Please check the ID and try again.")
|
|
return
|
|
|
|
# Check if media is already available
|
|
if media_details.get('mediaInfo'):
|
|
status = media_details.get('mediaInfo', {}).get('status')
|
|
if status == 5: # 5 = Available
|
|
await ctx.send(f"**{title}** is already available in your library! No need to request it.")
|
|
return
|
|
elif status == 4: # 4 = Partially Available (for TV shows)
|
|
if media_type.lower() == 'tv':
|
|
await ctx.send(f"**{title}** is partially available in your library. You might want to request specific seasons instead.")
|
|
elif status == 3: # 3 = Processing
|
|
await ctx.send(f"**{title}** is currently being processed. Please wait for it to become available.")
|
|
return
|
|
elif status == 2: # 2 = Pending
|
|
await ctx.send(f"**{title}** has already been requested and is pending approval or processing.")
|
|
return
|
|
|
|
# Create the request
|
|
request_data = {
|
|
'media_type': media_type.lower(),
|
|
'media_id': media_id,
|
|
'is_4k': False
|
|
}
|
|
|
|
if media_type.lower() == 'tv' and seasons:
|
|
request_data['seasons'] = seasons
|
|
|
|
logger.info(f"Submitting request for {media_type} ID {media_id}")
|
|
|
|
# Submit request
|
|
request = await api.create_request(**request_data)
|
|
|
|
# Build response embed
|
|
if year:
|
|
title = f"{title} ({year})"
|
|
|
|
request_type = "Movie" if media_type.lower() == "movie" else "TV Show"
|
|
|
|
embed = discord.Embed(
|
|
title=f"{request_type} Request Submitted",
|
|
description=f"**{title}** has been requested!",
|
|
color=self.embed_color
|
|
)
|
|
|
|
# Add info about which account was used
|
|
embed.set_footer(text=f"Requested using Jellyseerr account: {config.JELLYSEERR_EMAIL}")
|
|
|
|
status = request.get('status', 0)
|
|
status_map = {
|
|
1: "Pending Approval",
|
|
2: "Approved",
|
|
3: "Declined"
|
|
}
|
|
status_text = status_map.get(status, "Unknown")
|
|
|
|
embed.add_field(name="Status", value=status_text, inline=True)
|
|
|
|
if media_type.lower() == 'tv' and seasons and seasons != 'all':
|
|
embed.add_field(name="Requested Seasons", value=", ".join(str(s) for s in seasons), inline=True)
|
|
|
|
# Add poster if available
|
|
poster_path = media_details.get('posterPath')
|
|
if poster_path:
|
|
embed.set_thumbnail(url=f"https://image.tmdb.org/t/p/w500{poster_path}")
|
|
|
|
await ctx.send(embed=embed)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error creating request: {str(e)}", exc_info=True)
|
|
|
|
error_message = str(e)
|
|
if "500" in error_message:
|
|
if "Request already exists" in error_message:
|
|
await ctx.send(f"This media has already been requested and is either pending or being processed.")
|
|
else:
|
|
await ctx.send(f"Error creating request: Server error (500). This could be because:\n"
|
|
f"1. You might not have permission to request this media\n"
|
|
f"2. The media ID doesn't exist or is invalid\n"
|
|
f"3. The media may already be requested or available\n\n"
|
|
f"Try using `{config.BOT_PREFIX}info {media_id}` to check the current status")
|
|
elif "401" in error_message or "403" in error_message:
|
|
await ctx.send(f"Error creating request: Authentication error. The bot account may not have permission to make requests.")
|
|
else:
|
|
await ctx.send(f"Error creating request: {str(e)}")
|
|
# Provide more specific error messages for common issues
|
|
if "already exists" in str(e).lower():
|
|
await ctx.send(f"This media has already been requested or is already available in your library.")
|
|
elif "not found" in str(e).lower():
|
|
await ctx.send(f"Media not found. Please check the ID and try again.")
|
|
else:
|
|
await ctx.send(f"Error creating request: {str(e)}")
|
|
|
|
@commands.command(name="requests", aliases=["reqs"])
|
|
async def list_requests(self, ctx, status: str = "pending", page = 1):
|
|
"""
|
|
List media requests with optional status filter
|
|
Statuses: all, pending, approved, available, processing
|
|
"""
|
|
valid_statuses = ["all", "pending", "approved", "available", "processing"]
|
|
|
|
if status.lower() not in valid_statuses:
|
|
await ctx.send(f"Invalid status. Use one of: {', '.join(valid_statuses)}")
|
|
return
|
|
|
|
page = safe_int_convert(page, 1)
|
|
if page <= 0:
|
|
await ctx.send("Page number must be a positive integer.")
|
|
return
|
|
|
|
async with ctx.typing():
|
|
try:
|
|
# Get requests with the specified filter
|
|
api = get_api()
|
|
requests_data = await api.get_requests(filter_status=status.lower(), page=page)
|
|
|
|
results = requests_data.get('results', [])
|
|
if not results:
|
|
await ctx.send(f"No {status} requests found.")
|
|
return
|
|
|
|
# Build embed response
|
|
embed = discord.Embed(
|
|
title=f"{status.capitalize()} Media Requests",
|
|
color=self.embed_color
|
|
)
|
|
|
|
for req in results:
|
|
media = req.get('media', {})
|
|
media_type = media.get('mediaType', 'unknown')
|
|
|
|
# Get title based on media type
|
|
if media_type == 'movie':
|
|
title = media.get('title', 'Unknown Movie')
|
|
else:
|
|
title = media.get('name', 'Unknown TV Show')
|
|
|
|
# Get request details
|
|
request_id = req.get('id')
|
|
status_code = req.get('status', 0)
|
|
status_map = {
|
|
1: "📝 Pending",
|
|
2: "✅ Approved",
|
|
3: "❌ Declined",
|
|
4: "⏳ Processing"
|
|
}
|
|
status_text = status_map.get(status_code, "Unknown")
|
|
|
|
# Get requester
|
|
requested_by = req.get('requestedBy', {}).get('username', 'Unknown')
|
|
|
|
# Format requested date
|
|
created_at = req.get('createdAt', '')
|
|
request_date = created_at.split('T')[0] if created_at else 'Unknown'
|
|
|
|
# Format seasons for TV shows
|
|
seasons_text = ""
|
|
if media_type == 'tv' and req.get('seasons'):
|
|
seasons = req.get('seasons', [])
|
|
if seasons:
|
|
seasons_str = ", ".join(str(s) for s in seasons)
|
|
seasons_text = f"\nSeasons: {seasons_str}"
|
|
|
|
embed.add_field(
|
|
name=f"{title} ({media_type.upper()})",
|
|
value=f"Request ID: {request_id}\n"
|
|
f"Status: {status_text}\n"
|
|
f"Requested by: {requested_by}\n"
|
|
f"Date: {request_date}{seasons_text}",
|
|
inline=False
|
|
)
|
|
|
|
# Add pagination info if available
|
|
page_info = requests_data.get('pageInfo', {})
|
|
if page_info:
|
|
total_pages = page_info.get('pages', 1)
|
|
total_results = page_info.get('results', 0)
|
|
embed.set_footer(text=f"Page {page} of {total_pages} • {total_results} total requests")
|
|
|
|
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
|
|
|
|
# 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)) |