"""Plex integration with the bot.""" from math import floor, ceil import time import asyncio import requests import imdb import discord import xmltodict from discord_slash.utils.manage_components import (create_button, create_actionrow) from discord_slash.model import ButtonStyle class Plex(): """Container for Plex functions and commands.""" def __init__(self,bot): self.bot = bot self.credentials = self.bot.credentials self.long_strings = self.bot.long_strings server_ip = ["localhost", "192.168.0.40"][self.bot.options["testing"]] self.radarr_url = "http://"+server_ip+":7878/api/v3/" self.sonarr_url = "http://"+server_ip+":8989/api/" self.qbittorrent_url = "http://"+server_ip+":8080/api/v2/" self.movie_path = "/media/plex/Server/movies/" self.show_path = "/media/plex/Server/Shows/" async def request_movie(self, ctx, movie_name): """Request a movie for the Plex Server""" await self.bot.defer(ctx) self.bot.log("Searching for "+movie_name) movie_list = imdb.IMDb().search_movie(movie_name) movies = [] for movie in movie_list: if movie["kind"] == "movie": movies.append(movie) if len(movies) > 5: movies = movies[:5] if len(movies) == 1: message_title = "**Is it this movie?**" else: message_title = "**Is it any of these movies?**" message_text = "" imdb_ids = [] for i, movie in enumerate(movies): try: message_text += "\n"+str(i+1)+") "+movie["title"] try: message_text += " ("+str(movie["year"])+")" except KeyError: self.bot.log(f"{movie['title']} has no year.") except KeyError: message_text += "Error" imdb_ids.append(movie.movieID) self.bot.log( f"Returning a list of {len(movies)} possible movies: {imdb_ids}" ) embed = discord.Embed( title=message_title, description=message_text, colour=0x00FF00 ) buttons = [] if len(movies) == 1: buttons.append(create_button( style=ButtonStyle.green, label="✓", custom_id=f"plex:movie:{imdb_ids[0]}" ) ) else: for i in range(len(movies)): buttons.append( create_button( style=ButtonStyle.blue, label=str(i+1), custom_id=f"plex:movie:{imdb_ids[i]}" ) ) buttons.append(create_button( style=ButtonStyle.red, label="X", custom_id="plex:movie:" ) ) action_rows = [] for i in range(((len(buttons)-1)//5)+1): action_rows.append( create_actionrow( *buttons[(i*5):(min(len(buttons),i*5+5))] ) ) await ctx.send(embed=embed, components=action_rows) async def add_movie(self, message, imdb_id, edit_message = True): """Add a movie to Plex server.""" if not edit_message: await message.delete() if imdb_id == "": self.bot.log("Did not find what the user was searching for") message_text = "Try searching for the IMDB id" else: self.bot.log("Trying to add movie "+str(imdb_id)) # Searches for the movie using the imdb id through Radarr api_key = self.credentials["radarr_key"] request_url = self.radarr_url+"movie/lookup/imdb?imdbId=tt"+imdb_id request_url += "&apiKey="+api_key response = requests.get(request_url) # Makes the dict used for the post request lookup_data = response.json() post_data = { "qualityProfileId": 1, "rootFolderPath" : self.movie_path, "monitored" : True, "addOptions": {"searchForMovie": True} } for key in ["tmdbId","title","titleSlug","images","year"]: post_data.update({key : lookup_data[key]}) # Makes the post request response = requests.post( url = self.radarr_url+"movie?apikey="+api_key, json = post_data ) # Deciphers the response if response.status_code == 201: self.bot.log("Added "+post_data["title"]+" to Plex") message_text = "{} successfully added to Plex".format( post_data["title"] ) elif response.status_code == 400: self.bot.log("The movie was already on plex") message_text = self.long_strings["Already on Plex"].format( post_data['title'] ) else: self.bot.log(str(response.status_code)+" "+response.reason) message_text = "Something went wrong", if edit_message: await message.edit( embed = None, content = message_text, components = [] ) else: await message.channel.send(message_text) async def request_show(self, ctx, show_name): """Request a show for the Plex server.""" await self.bot.defer(ctx) self.bot.log("Searching for "+show_name) movies = imdb.IMDb().search_movie(show_name) # Replace with tvdb shows = [] for movie in movies: if movie["kind"] in ["tv series","tv miniseries"]: shows.append(movie) if len(shows) > 5: shows = shows[:5] if len(shows) == 1: message_title = "**Is it this show?**" else: message_title = "**Is it any of these shows?**" message_text = "" imdb_ids = [] for i, show in enumerate(shows): try: message_text += f"\n{i+1}) {show['title']} ({show['year']})" except KeyError: try: message_text += "\n"+str(i+1)+") "+show["title"] except KeyError: message_text += "Error" imdb_ids.append(show.movieID) self.bot.log( f"Returning a list of {len(shows)} possible shows: {imdb_ids}" ) embed = discord.Embed( title=message_title, description=message_text, colour=0x00FF00 ) buttons = [] if len(shows) == 1: buttons.append(create_button( style=ButtonStyle.green, label="✓", custom_id=f"plex:show:{imdb_ids[0]}" ) ) else: for i in range(len(shows)): buttons.append( create_button( style=ButtonStyle.blue, label=str(i+1), custom_id=f"plex:show:{imdb_ids[i]}" ) ) buttons.append(create_button( style=ButtonStyle.red, label="X", custom_id="plex:show:" ) ) action_rows = [] for i in range(((len(buttons)-1)//5)+1): action_rows.append( create_actionrow( *buttons[(i*5):(min(len(buttons),i*5+5))] ) ) await ctx.send(embed=embed, components=action_rows) async def add_show(self, message, imdb_id, edit_message = True): """Add the requested show to Plex.""" if imdb_id == "": self.bot.log("Did not find what the user was searching for") message_text = "Try searching for the IMDB id" else: self.bot.log("Trying to add show "+str(imdb_id)) # Finds the tvdb id tvdb_api_url = "https://thetvdb.com/api/" tvdb_method = "GetSeriesByRemoteID.php" tvdb_request_url = f"{tvdb_api_url}{tvdb_method}" tvdb_id = xmltodict.parse( requests.get( tvdb_request_url+f"?imdbid=tt{imdb_id}", headers = {"ContentType" : "application/json"} ).text )['Data']['Series']['seriesid'] # Finds the rest of the information using Sonarr api_key = self.credentials["sonarr_key"] request_url = self.sonarr_url+"series/lookup?term=" request_url += f"tvdb:{tvdb_id}" request_url += "&apiKey="+api_key response = requests.get(request_url) # Makes the dict used for the post request lookup_data = response.json()[0] post_data = { "ProfileId" : 1, "rootFolder_path" : self.show_path, "monitored" : True, "addOptions" : {"searchForMissingEpisodes" : True} } for key in ["tvdbId","title","titleSlug","images","seasons"]: post_data.update({key : lookup_data[key]}) # Makes the post request response = requests.post( url= self.sonarr_url+"series?apikey="+api_key, json = post_data ) # Deciphers the response if response.status_code == 201: self.bot.log("Added a "+post_data["title"]+" to Plex") message_text = post_data["title"]+" successfully added to Plex" elif response.status_code == 400: message_text = self.long_strings["Already on Plex"].format( post_data['title'] ) else: self.bot.log(str(response.status_code)+" "+response.reason) message_text = "Something went wrong" if edit_message: await message.edit( embed = None, content = message_text, components = [] ) else: await message.channel.send(message_text) async def __generate_download_list(self, show_dm, show_movies, show_shows, episodes): """Generate a list of all torrents. *Returns* message_text: str A formatted list of all torrents all_downloaded: bool Whether all torrents are downloaded """ self.bot.log("Generating torrent list") title_width = 100 message = [] all_downloaded = True if show_dm: message.append("") dm_section_title = "*Torrent Downloads*" dm_section_title_line = "-"*((title_width-len(dm_section_title))//2) message.append( dm_section_title_line+dm_section_title+dm_section_title_line ) login_url = self.qbittorrent_url+"auth/login" username = self.credentials["qbittorrent_username"] password = self.credentials["qbittorrent_password"] login_url += f"?username={username}&password={password}" cookie = {"SID": requests.get(login_url).cookies.values()[0]} response = requests.get( self.qbittorrent_url+"torrents/info", cookies=cookie ) torrent_list = response.json() if len(torrent_list) > 0: for torrent in torrent_list: torrent_name = torrent["name"] if len(torrent_name) > 30: if torrent_name[26] == " ": torrent_name = torrent_name[:26]+"...." else: torrent_name = torrent_name[:27]+"..." while len(torrent_name) < 30: torrent_name += " " if torrent["size"] == 0: download_ratio = 0 elif torrent["amount_left"] == 0: download_ratio = 1 else: download_ratio = min( torrent["downloaded"]/torrent["size"], 1 ) progress_bar = "|"+("█"*floor(download_ratio*20)) while len(progress_bar) < 21: progress_bar += " " progress_bar += "| "+str(floor(download_ratio*100))+"%" while len(progress_bar) < 27: progress_bar += " " eta_in_seconds = torrent["eta"] if eta_in_seconds >= 8640000: eta = "∞" else: eta = "" if eta_in_seconds >= 86400: eta += str(floor(eta_in_seconds/86400))+"d " if eta_in_seconds >= 3600: eta += str(floor((eta_in_seconds%86400)/3600))+"h " if eta_in_seconds >= 60: eta += str(floor((eta_in_seconds%3600)/60))+"m " eta += str(eta_in_seconds%60)+"s" torrent_info = f"{torrent_name} {progress_bar} " torrent_info += f"(Eta: {eta})" if torrent["state"] == "stalledDL": torrent_info += " (Stalled)" if not (download_ratio == 1 and torrent["last_activity"] < time.time()-7200): message.append(torrent_info) if download_ratio < 1 and torrent["state"] != "stalledDL": all_downloaded = False else: message.append("No torrents currently downloading") if show_movies: message.append("") movies_section_title = "*Missing movies not downloading*" movies_section_line = ( "-"*((title_width-len(movies_section_title))//2) ) message.append( movies_section_line+movies_section_title+movies_section_line ) movie_list = requests.get( self.radarr_url+"movie?apiKey="+self.credentials["radarr_key"] ).json() print( self.radarr_url+"movie?apiKey="+self.credentials["radarr_key"] ) movie_queue = requests.get( self.radarr_url+"queue?apiKey="+self.credentials["radarr_key"] ).json() movie_queue_ids = [] for queue_item in movie_queue["records"]: movie_queue_ids.append(queue_item["movieId"]) for movie in movie_list: if (not movie["hasFile"] and movie["id"] not in movie_queue_ids): movie_name = movie["title"] if len(movie_name) > 40: if movie_name[36] == " ": movie_name = movie_name[:36]+"...." else: movie_name = movie_name[:37]+"..." while len(movie_name) < 41: movie_name += " " if movie["monitored"]: movie_info = movie_name+"Could not find a torrent" else: movie_info = self.long_strings["No torrent"].format( movie_name ) message.append(movie_info) if show_shows: message.append("") show_section_title = "*Missing shows not downloading*" show_section_line = "-"*((title_width-len(show_section_title))//2) message.append( show_section_line+show_section_title+show_section_line ) show_list = requests.get( self.sonarr_url+"series?apiKey="+self.credentials["sonarr_key"] ).json() for show in show_list: if show["seasons"][0]["seasonNumber"] == 0: seasons = show["seasons"][1:] else: seasons = show["seasons"] if any( ( i["statistics"]["episodeCount"] != i["statistics"]["totalEpisodeCount"] ) for i in seasons): if all( i["statistics"]["episodeCount"] == 0 for i in seasons ): message.append(show["title"] + " (all episodes)") else: if episodes: missing_episodes = sum( (i["statistics"]["totalEpisodeCount"] - i["statistics"]["episodeCount"]) for i in seasons) message.append( f"{show['title']} ({missing_episodes} episodes)" ) message.append("-"*title_width) message_text = "```"+"\n".join(message[1:])+"```" if message_text == "``````": message_text = self.long_strings["No torrents downloading"] return message_text, all_downloaded async def downloading(self, ctx, content): """Send message with list of all downloading torrents.""" async def send_long_message(ctx,message_text): if len(message_text) <= 1994: await ctx.send("```"+message_text+"```") else: cut_off_index = message_text[:1994].rfind("\n") await ctx.send("```"+message_text[:cut_off_index]+"```") await send_long_message(ctx,message_text[cut_off_index+1:]) await self.bot.defer(ctx) # showDM, showMovies, showShows, episodes parameters = [False, False, False, False] show_dm_args = ["d", "dm", "downloading", "downloadmanager"] show_movies_args = ["m", "movies"] show_shows_args = ["s", "shows", "series"] show_episode_args = ["e", "episodes"] arg_list = [ show_dm_args, show_movies_args, show_shows_args, show_episode_args ] input_args = [] valid_arguments = True while content != "" and valid_arguments: if content[0] == " ": content = content[1:] elif content[0] == "-": if content[1] == "-": arg_start = 2 if " " in content: arg_stop = content.find(" ") else: arg_stop = None else: arg_start = 1 arg_stop = 2 input_args.append(content[arg_start:arg_stop]) if arg_stop is None: content = "" else: content = content[arg_stop:] else: valid_arguments = False if valid_arguments: for arg_index, arg_aliases in enumerate(arg_list): arg_in_input = [i in input_args for i in arg_aliases] if any(arg_in_input): input_args.remove(arg_aliases[arg_in_input.index(True)]) parameters[arg_index] = True if len(input_args) != 0 or (not parameters[2] and parameters[3]): valid_arguments = False show_anything = any(i for i in parameters) if not (valid_arguments and show_anything): await ctx.send(self.long_strings["Invalid parameters"]) else: message_text, all_downloaded = await self.__generate_download_list( *parameters ) if not message_text.startswith("```"): await ctx.send(message_text) elif len(message_text) > 2000: message_text = message_text[3:-3] await send_long_message(ctx,message_text) elif all_downloaded: await ctx.send(message_text) else: updates_left = 60 message_text = self.long_strings["Update"].format( message_text[:-3], ceil(updates_left/6) ) old_message = await ctx.send(message_text) while ((not all_downloaded) and updates_left > 0): await asyncio.sleep(10) updates_left -= 1 message_text, all_downloaded = await ( self.__generate_download_list(*parameters) ) message_text = self.long_strings["Update"].format( message_text[:-3], ceil(updates_left/6) ) await old_message.edit(content = message_text) message_text, all_downloaded = await ( self.__generate_download_list(*parameters) ) if message_text.startswith("```"): if all_downloaded: self.bot.log("All torrents are downloaded") else: message_text = self.long_strings["No updates"].format( message_text[:-3] ) self.bot.log("The message updated 20 times") await old_message.edit(content = message_text)