From d703658be341199d96a4c32e043d9230eb56a2dc Mon Sep 17 00:00:00 2001 From: pacnpal Date: Mon, 30 Sep 2024 20:08:21 -0400 Subject: [PATCH] Add Overseerr cog --- overseerr/LICENSE | 21 ++++++ overseerr/README.md | 35 ++++++++++ overseerr/__init__.py | 7 ++ overseerr/overseerr.py | 152 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 215 insertions(+) create mode 100644 overseerr/LICENSE create mode 100644 overseerr/README.md create mode 100644 overseerr/__init__.py create mode 100644 overseerr/overseerr.py diff --git a/overseerr/LICENSE b/overseerr/LICENSE new file mode 100644 index 0000000..963bf1d --- /dev/null +++ b/overseerr/LICENSE @@ -0,0 +1,21 @@ +Creative Commons Attribution 4.0 International License + +This work is licensed under the Creative Commons Attribution 4.0 International License. + +To view a copy of this license, visit: +http://creativecommons.org/licenses/by/4.0/ + +or send a letter to: +Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. + +The full text of the license can be found at: +https://creativecommons.org/licenses/by/4.0/legalcode + +You are free to: +- Share — copy and redistribute the material in any medium or format +- Adapt — remix, transform, and build upon the material for any purpose, even commercially. + +Under the following terms: +- Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use. + +No additional restrictions — You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits. diff --git a/overseerr/README.md b/overseerr/README.md new file mode 100644 index 0000000..ac28795 --- /dev/null +++ b/overseerr/README.md @@ -0,0 +1,35 @@ +# Overseerr Cog for Red Discord Bot + +This cog allows interaction with [Overseerr](https://overseerr.dev/) directly from Discord. Users can search for movies or TV shows, request them, and have admins approve requests. It's designed for servers with Overseerr set up for managing media requests. + +## Features +- **Set Overseerr URL and API key**: Admins can configure the Overseerr URL and API key for API interactions. +- **Search and request media**: Users can search for movies or TV shows and request them directly in Discord. +- **Media availability status**: The cog checks if media is already available or has been requested before making new requests. +- **Approve requests**: Admins with the appropriate role can approve Overseerr requests within Discord. + +## Commands + +### Admin Commands +- **`[p]setoverseerr `** + - Set the Overseerr URL and API key for the bot to communicate with Overseerr. + - Example: `[p]setoverseerr https://my.overseerr.url abcdefghijklmnop` + +- **`[p]setadminrole `** + - Set the name of the admin role that is allowed to approve Overseerr requests. + - Example: `[p]setadminrole Overseerr Admin` + +### User Commands +- **`[p]request `** + - Search for a movie or TV show and request it if it's not already available or requested. + - Example: `[p]request The Matrix` + +- **`[p]approve `** + - Approve a media request by its request ID (requires the admin role). + - Example: `[p]approve 123` + +## Installation + +1. Add the cog to your Red instance: + ```bash + [p]load overseerr diff --git a/overseerr/__init__.py b/overseerr/__init__.py new file mode 100644 index 0000000..38736ea --- /dev/null +++ b/overseerr/__init__.py @@ -0,0 +1,7 @@ +from .overseerr import Overseerr + +__red_end_user_data_statement__ = "This allows users to make requests to Overseerr and Admins can approve them." + + +async def setup(bot): + await bot.add_cog(Overseerr(bot)) \ No newline at end of file diff --git a/overseerr/overseerr.py b/overseerr/overseerr.py new file mode 100644 index 0000000..d88e116 --- /dev/null +++ b/overseerr/overseerr.py @@ -0,0 +1,152 @@ +from redbot.core import commands, Config +from redbot.core.bot import Red +import asyncio +import json + +class Overseerr(commands.Cog): + def __init__(self, bot: Red): + self.bot = bot + self.config = Config.get_conf(self, identifier=1234567890) + default_global = { + "overseerr_url": None, + "overseerr_api_key": None, + "admin_role_name": "Overseerr Admin" + } + self.config.register_global(**default_global) + + @commands.command() + @commands.admin() + async def setoverseerr(self, ctx: commands.Context, url: str, api_key: str): + """Set the Overseerr URL and API key.""" + await self.config.overseerr_url.set(url) + await self.config.overseerr_api_key.set(api_key) + await ctx.send("Overseerr URL and API key have been set.") + + @commands.command() + @commands.admin() + async def setadminrole(self, ctx: commands.Context, role_name: str): + """Set the admin role name for Overseerr approvals.""" + await self.config.admin_role_name.set(role_name) + await ctx.send(f"Admin role for Overseerr approvals set to {role_name}.") + + async def get_media_status(self, media_id, media_type): + overseerr_url = await self.config.overseerr_url() + overseerr_api_key = await self.config.overseerr_api_key() + url = f"{overseerr_url}/api/v1/{'movie' if media_type == 'movie' else 'tv'}/{media_id}" + headers = {"X-Api-key": overseerr_api_key} + + async with self.bot.session.get(url, headers=headers) as resp: + if resp.status == 200: + data = await resp.json() + status = "Available" if data.get('mediaInfo', {}).get('status') == 3 else "Not Available" + if data.get('request'): + status += " (Requested)" + return status + return "Status Unknown" + + @commands.command() + async def request(self, ctx: commands.Context, *, query: str): + """Search and request a movie or TV show on Overseerr.""" + overseerr_url = await self.config.overseerr_url() + overseerr_api_key = await self.config.overseerr_api_key() + + if not overseerr_url or not overseerr_api_key: + await ctx.send("Overseerr is not configured. Please ask an admin to set it up.") + return + + search_url = f"{overseerr_url}/api/v1/search" + request_url = f"{overseerr_url}/api/v1/request" + + headers = { + "X-Api-Key": overseerr_api_key, + "Content-Type": "application/json" + } + + # Search for the movie or TV show + async with self.bot.session.get(search_url, headers=headers, params={"query": query}) as resp: + search_results = await resp.json() + + if not search_results['results']: + await ctx.send(f"No results found for '{query}'.") + return + + # Display search results with availability status + result_message = "Please choose a result by reacting with the corresponding number:\n\n" + for i, result in enumerate(search_results['results'][:5], start=1): + media_type = result['mediaType'] + status = await self.get_media_status(result['id'], media_type) + result_message += f"{i}. [{media_type.upper()}] {result['title']} ({result.get('releaseDate', 'N/A')}) - {status}\n" + + result_msg = await ctx.send(result_message) + + # Add reaction options + reactions = ['1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣'] + for i in range(min(len(search_results['results']), 5)): + await result_msg.add_reaction(reactions[i]) + + def check(reaction, user): + return user == ctx.author and str(reaction.emoji) in reactions + + try: + reaction, user = await self.bot.wait_for('reaction_add', timeout=60.0, check=check) + except asyncio.TimeoutError: + await ctx.send("Search timed out. Please try again.") + return + + selected_index = reactions.index(str(reaction.emoji)) + selected_result = search_results['results'][selected_index] + media_type = selected_result['mediaType'] + + # Check if the media is already available or requested + status = await self.get_media_status(selected_result['id'], media_type) + if "Available" in status: + await ctx.send(f"'{selected_result['title']}' is already available. No need to request!") + return + elif "Requested" in status: + await ctx.send(f"'{selected_result['title']}' has already been requested. No need to request again!") + return + + # Make the request + request_data = { + "mediaId": selected_result['id'], + "mediaType": media_type + } + + async with self.bot.session.post(request_url, headers=headers, json=request_data) as resp: + if resp.status == 200: + response_data = await resp.json() + request_id = response_data.get('id') + await ctx.send(f"Successfully requested {media_type} '{selected_result['title']}'! Request ID: {request_id}") + else: + await ctx.send(f"Failed to request {media_type} '{selected_result['title']}'. Please try again later.") + + @commands.command() + async def approve(self, ctx: commands.Context, request_id: int): + """Approve a request on Overseerr.""" + admin_role_name = await self.config.admin_role_name() + if not any(role.name == admin_role_name for role in ctx.author.roles): + await ctx.send(f"You need the '{admin_role_name}' role to approve requests.") + return + + overseerr_url = await self.config.overseerr_url() + overseerr_api_key = await self.config.overseerr_api_key() + + if not overseerr_url or not overseerr_api_key: + await ctx.send("Overseerr is not configured. Please ask an admin to set it up.") + return + + approve_url = f"{overseerr_url}/api/v1/request/{request_id}/approve" + + headers = { + "X-Api-Key": overseerr_api_key, + "Content-Type": "application/json" + } + + async with self.bot.session.post(approve_url, headers=headers) as resp: + if resp.status == 200: + await ctx.send(f"Request {request_id} has been approved!") + else: + await ctx.send(f"Failed to approve request {request_id}. Please check the request ID and try again.") + +def setup(bot: Red): + bot.add_cog(Overseerr(bot))