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} ` 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 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))