First working version of jellyseerr discord bot
This commit is contained in:
584
commands.py
Normal file
584
commands.py
Normal file
@@ -0,0 +1,584 @@
|
||||
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():
|
||||
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")
|
||||
return jellyseerr_api
|
||||
|
||||
def safe_int_convert(value, default=None):
|
||||
"""Safely convert a value to integer, returning default if conversion fails"""
|
||||
if value is None:
|
||||
return default
|
||||
try:
|
||||
return int(value)
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
|
||||
class MediaCommands(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
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)"""
|
||||
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
|
||||
if not media_info or 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
|
||||
if not media_info or 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 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"""
|
||||
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 Exception as e:
|
||||
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"""
|
||||
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 Exception as e:
|
||||
await ctx.send(f"Error retrieving trending media: {str(e)}")
|
||||
|
||||
class RequestCommands(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
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
|
||||
|
||||
# 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:
|
||||
await ctx.send(f"Error creating request: Server error (500). This could be because:\n"
|
||||
f"1. You might not have permission to request this media\n"
|
||||
f"2. The media ID doesn't exist or is invalid\n"
|
||||
f"3. The media may already be requested or available\n\n"
|
||||
f"Try searching for the media first with `{config.BOT_PREFIX}search` to get a valid ID")
|
||||
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)}")
|
||||
|
||||
@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 Exception as e:
|
||||
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):
|
||||
bot.add_cog(MediaCommands(bot))
|
||||
bot.add_cog(RequestCommands(bot))
|
||||
bot.add_cog(UtilityCommands(bot))
|
||||
Reference in New Issue
Block a user