def series_modal(request): content_discovery = ContentDiscovery() content_manager = ContentManager() report_modal = request.GET.get("report_modal", False) # Get the ID from the URL tmdb_id = request.GET.get("tmdb_id", None) tvdb_id = request.GET.get("tvdb_id", None) # Determine the TVDB ID if tvdb_id: pass elif tmdb_id: tvdb_id = content_discovery.get_external_ids(tmdb_id, "tv")["tvdb_id"] # Check if the show is already within Sonarr's collection requested_show = content_manager.get(tvdb_id=tvdb_id) # If it doesn't already exists, add then add it if requested_show is None: sonarr_params = obtain_sonarr_parameters(content_discovery, content_manager, tmdb_id, tvdb_id) requested_show = content_manager.add( tvdb_id=tvdb_id, quality_profile_id=sonarr_params["sonarr_profile_id"], root_dir=sonarr_params["sonarr_root"], series_type=sonarr_params["series_type"], season_folders=sonarr_params["season_folders"], ) # Keep refreshing until we get the series from Sonarr series = content_manager.get(tvdb_id=tvdb_id, obtain_season_info=True, force_update_cache=True) if series is None: series_fetch_retries = 0 while series is None: if series_fetch_retries > MAX_SERIES_FETCH_RETRIES: break series_fetch_retries = series_fetch_retries + 1 series = content_manager.get(tvdb_id=tvdb_id, obtain_season_info=True, force_update_cache=True) log.handler( "Sonarr did not have the series information! Conreq is waiting...", log.INFO, __logger, ) context = generate_context({ "seasons": series["seasons"], "tvdb_id": tvdb_id, "report_modal": report_modal }) template = loader.get_template("modal/series_selection.html") return HttpResponse(template.render(context, request))
def set_many_availability(results): """Sets the availabily on list of cards.""" # Fetch Sonarr and Radarr libraries radarr_library = cache.handler( "radarr library", ) sonarr_library = cache.handler( "sonarr library", ) # Generate the availability if possible, or get the external ID if a TVDB ID is needed thread_list = [] if isinstance(results, list): for card in results: thread = Thread( target=__set_many_availability, args=[card, radarr_library, sonarr_library], ) thread.start() thread_list.append(thread) for thread in thread_list: thread.join() else: log.handler( "set_many_availability did not recieve a List!", log.WARNING, _logger, ) return results
def manage_issue(request): if request.method == "POST": request_parameters = json.loads(request.body.decode("utf-8")) log.handler( "Manage issue command received: " + str(request_parameters), log.INFO, _logger, ) # Delete a request if request_parameters.get("action", None) == "delete": issue = ReportedIssue.objects.select_related("reported_by").get( id=request_parameters["request_id"]) # Check if report belongs to the user, or if the user is admin if issue and request.user.is_staff or issue.reported_by == request.user: issue.delete() return JsonResponse({"success": True}) # Change the resolved status of a request elif (request_parameters.get("action", None) == "resolve" and request.user.is_staff): issue = ReportedIssue.objects.filter( id=request_parameters["request_id"]) if issue: issue.update(resolved=request_parameters["resolved"]) return JsonResponse({"success": True}) return HttpResponseForbidden()
def get_sonarr_library(self): """Fetches everything within Sonarr's library""" try: if self.conreq_config.sonarr_enabled: if self.conreq_config.sonarr_url and self.conreq_config.sonarr_api_key: # Get the latest list of Sonarr's collection results = self.__sonarr.getSeries() # Set up a dictionary of results with IDs as keys results_with_ids = {} for series in results: if series.__contains__("tvdbId"): self.__check_availability(series) self.__set_content_attributes("tv", "sonarr", series) results_with_ids[str(series["tvdbId"])] = series # Return all movies return results_with_ids log.handler( "Sonarr URL or API key is unset!", log.WARNING, _logger, ) except: log.handler( "Could not get series!", log.ERROR, _logger, )
async def receive_json(self, content, **kwargs): """When the browser attempts to send a message to the server.""" log.handler( content, log.INFO, self.__logger, ) # Reject users that aren't logged in if (isinstance(self.scope["user"], AnonymousUser) or not self.scope["user"].is_authenticated): await self.__forbidden() else: # Verify login status. await login(self.scope, self.scope["user"]) # Process the command if content["command_name"] == "request": await self.__request_content(content) elif content["command_name"] == "generate modal": await self.__generate_modal(content) elif content["command_name"] == "server settings": await self.__server_settings(content) else: log.handler( "Invalid websocket command.", log.ERROR, self.__logger, )
def get_all_sonarr_content(self): try: if self.conreq_config.sonarr_enabled: if self.conreq_config.sonarr_url and self.conreq_config.sonarr_api_key: # Get the latest list of Sonarr's collection results = self.__sonarr.getSeries() # Set up a dictionary of results with IDs as keys results_with_ids = {} for series in results: if series.__contains__("tvdbId"): self.__check_status(series) results_with_ids[str(series["tvdbId"])] = series # Return all movies return results_with_ids log.handler( "Sonarr URL or API key is unset!", log.WARNING, self.__logger, ) except: log.handler( "Could not get series!", log.ERROR, self.__logger, )
def all(self, query, page_number): """Search for a query. Sort the results based on their similiarity to the query. Args: query: A string containing a search term. """ try: results = self._remove_bad_content_types( self._set_content_attributes( None, cache.handler( "search all", page_key=page_number, function=tmdb.Search().multi, cache_duration=SEARCH_CACHE_TIMEOUT, kwargs={ "page": page_number, "query": query, "language": LANGUAGE, }, ), )) return results except: log.handler( "Searching for all failed!", log.ERROR, _logger, ) return []
def movie(self, query, page_number): """Search Radarr for a query. Args: query: A string containing a search term. """ try: results = self._set_content_attributes( "movie", cache.handler( "search movie", page_key=page_number, function=tmdb.Search().movie, cache_duration=SEARCH_CACHE_TIMEOUT, kwargs={ "page": page_number, "query": query, "language": LANGUAGE, }, ), ) return results except: log.handler( "Searching for movies failed!", log.ERROR, _logger, ) return []
def refresh_content(self): """Refreshes Sonarr and Radarr's content""" try: if self.conreq_config.radarr_enabled: cache.handler( "radarr library cache", function=self.get_all_radarr_content, cache_duration=GET_CONTENT_CACHE_TIMEOUT, force_update_cache=True, ) except: log.handler( "Failed to refresh radarr!", log.WARNING, self.__logger, ) try: if self.conreq_config.sonarr_enabled: cache.handler( "sonarr library cache", function=self.get_all_sonarr_content, cache_duration=GET_CONTENT_CACHE_TIMEOUT, force_update_cache=True, ) except: log.handler( "Failed to refresh sonarr!", log.WARNING, self.__logger, )
def __generate_conreq_rank(self, result, clean_query): """Determines string similarity and combined with a weight of the original rank""" try: clean_title = clean_string(result["title"]) # Multiplier if whole substring was found within the search result if clean_title.find(clean_query) != -1: query_substring_multiplier = 0.1 else: query_substring_multiplier = 1 # Generate similarity rank result["conreqSimilarityRank"] = ( # Round the values to look pretty self.__round( # String similarity between the query and result ( self.__damerau.distance(clean_query, clean_title) # Use sonarr/radarr's original rank as a weight/bias * (result["arrOriginalRank"] / 10)) # Bias towards full substring matches * query_substring_multiplier) + 1) except: log.handler("Failed to generate conreq rank!", log.ERROR, _logger) try: result["conreqSimilarityRank"] = result["arrOriginalRank"] except: result["conreqSimilarityRank"] = 1
def television(self, query, page_number): """Search Sonarr for a query. Args: query: A string containing a search term. conreq_rank: Calculate conreq similarity ranking and sort the results (True/False) """ try: results = self._set_content_attributes( "tv", cache.handler( "search tv", page_key=page_number, function=tmdb.Search().tv, cache_duration=SEARCH_CACHE_TIMEOUT, kwargs={ "page": page_number, "query": query, "language": LANGUAGE, }, ), ) return results except: log.handler( "Searching for TV failed!", log.ERROR, _logger, ) return []
def get_all_radarr_content(self): try: if self.conreq_config.radarr_enabled: if self.conreq_config.radarr_url and self.conreq_config.radarr_api_key: # Get the latest list of Radarr's collection results = self.__radarr.getMovie() # Set up a dictionary of results with IDs as keys results_with_ids = {} for movie in results: if movie.__contains__("tmdbId"): self.__check_status(movie) results_with_ids[str(movie["tmdbId"])] = movie # Return all movies return results_with_ids log.handler( "Radarr URL or API key is unset!", log.WARNING, self.__logger, ) except: log.handler( "Could not get movies!", log.ERROR, self.__logger, )
async def __generate_modal(self, content): response = { "command_name": "render page element", "selector": "#modal-content", "html": None, } # Check if an ID is present if (content["parameters"]["tvdb_id"] is not None or content["parameters"]["tmdb_id"] is not None): # Episode modal if content["parameters"]["modal_type"] == "episode selector": response["html"] = await database_sync_to_async(series_modal)( tmdb_id=content["parameters"]["tmdb_id"], tvdb_id=content["parameters"]["tvdb_id"], user=self.scope["user"], ) await self.send_json(response) # Content info modal elif content["parameters"]["modal_type"] == "content info": pass else: log.handler( "Invalid modal type!", log.ERROR, self.__logger, ) else: log.handler( "Generate modal command was missing an ID!", log.ERROR, self.__logger, )
def sonarr_request_background_task( tvdb_id, request_parameters, content_manager, sonarr_params, username ): """Function that can be run in the background to request something on Sonarr""" # Check if the show is already within Sonarr's collection show = content_manager.get(tvdb_id=tvdb_id) # If it doesn't already exists, add then request it if show is None: show = content_manager.add( tvdb_id=tvdb_id, quality_profile_id=sonarr_params["sonarr_profile_id"], root_dir=sonarr_params["sonarr_root"], series_type=sonarr_params["series_type"], season_folders=sonarr_params["season_folders"], ) # Request content_manager.request( sonarr_id=show["id"], seasons=request_parameters["seasons"], episode_ids=request_parameters["episode_ids"], ) log.handler( username + " requested TV series " + show["title"], log.INFO, _logger, )
def get_radarr_library(self): """Fetches everything within Radarr's library""" try: if self.conreq_config.radarr_enabled: if self.conreq_config.radarr_url and self.conreq_config.radarr_api_key: # Get the latest list of Radarr's collection results = self.__radarr.getMovie() # Set up a dictionary of results with IDs as keys results_with_ids = {} for movie in results: if movie.__contains__("tmdbId"): self.__check_availability(movie) self.__set_content_attributes("movie", "radarr", movie) results_with_ids[str(movie["tmdbId"])] = movie # Return all movies return results_with_ids log.handler( "Radarr URL or API key is unset!", log.WARNING, _logger, ) except: log.handler( "Could not get movies!", log.ERROR, _logger, )
def similar_and_recommended(self, tmdb_id, content_type): """Merges the results of similar and recommended. Args: id: An Integer or String containing the TMDB ID. content_type: String containing "movie" or "tv". """ try: thread_list = [] # Get recommended page one recommend_page_one = self.__recommended(tmdb_id, content_type, 1) # Gather additional recommended pages if recommend_page_one["total_pages"] > 1: for page_number in range(2, recommend_page_one["total_pages"]): if page_number <= MAX_RECOMMENDED_PAGES: thread = ReturnThread( target=self.__recommended, args=[tmdb_id, content_type, page_number], ) thread.start() thread_list.append(thread) # Get similar page one similar_page_one = self.__similar(tmdb_id, content_type, 1) # Gather up additional similar pages if similar_page_one["total_pages"] > 1: for page_number in range(2, similar_page_one["total_pages"]): if page_number <= MAX_SIMILAR_PAGES: thread = ReturnThread( target=self.__similar, args=[tmdb_id, content_type, page_number], ) thread.start() thread_list.append(thread) # Merge results of the first page of similar and recommended merged_results = self.__merge_results(recommend_page_one, similar_page_one) # Wait for all the threads to complete and merge them in for thread in thread_list: merged_results = self.__merge_results(merged_results, thread.join()) self.determine_id_validity(merged_results) # Shuffle and return return self.__shuffle_results(merged_results) except: log.handler( "Failed to obtain merged Similar and Recommended!", log.ERROR, self.__logger, ) return {}
def sonarr_settings(request): template = loader.get_template("viewport/components/server_settings_sonarr.html") # Database values conreq_config = ConreqConfig.get_solo() # Obtain sonarr and radarr information content_manger = ArrManager() sonarr_quality_profiles = [] current_sonarr_anime_quality_profile = "" current_sonarr_tv_quality_profile = "" sonarr_folders = [] current_sonarr_anime_folder = "" current_sonarr_tv_folder = "" if conreq_config.sonarr_enabled: # Sonarr Quality Profiles try: for profile in content_manger.sonarr_quality_profiles(): # Current anime profile if conreq_config.sonarr_anime_quality_profile == profile["id"]: current_sonarr_anime_quality_profile = profile["name"] # Current TV profile if conreq_config.sonarr_tv_quality_profile == profile["id"]: current_sonarr_tv_quality_profile = profile["name"] # List of all dropdown entries sonarr_quality_profiles.append( {"id": profile["id"], "label": profile["name"]} ) except: log.handler("Failed to obtain Sonarr Quality Profiles!", log.ERROR, _logger) # Sonarr Folder Paths try: for path in content_manger.sonarr_root_dirs(): # Current anime dirs if conreq_config.sonarr_anime_folder == path["id"]: current_sonarr_anime_folder = path["path"] # Current TV dirs if conreq_config.sonarr_tv_folder == path["id"]: current_sonarr_tv_folder = path["path"] # List of all dropdown entries sonarr_folders.append({"id": path["id"], "label": path["path"]}) except: log.handler("Failed to obtain Sonarr Folder Paths!", log.ERROR, _logger) context = { "sonarr_quality_profiles": sonarr_quality_profiles, "current_sonarr_anime_quality_profile": current_sonarr_anime_quality_profile, "current_sonarr_tv_quality_profile": current_sonarr_tv_quality_profile, "sonarr_folders": sonarr_folders, "current_sonarr_anime_folder": current_sonarr_anime_folder, "current_sonarr_tv_folder": current_sonarr_tv_folder, } return HttpResponse(template.render(context, request))
def get_by_tmdb_id(self, tmdb_id, content_type, obtain_extras=True): """Obtains a movie or series given a TMDB ID. Args: id: An Integer or String containing the TMDB ID. content_type: String containing "movie" or "tv". """ # Searches for content based on TMDB ID try: # Obtain extras if needed if obtain_extras: extras = "reviews,keywords,videos,credits,images" else: extras = None # Obtain a movie by ID if content_type == "movie": return self._set_content_attributes( content_type, cache.handler( "get movie by tmdb id", page_key=tmdb_id, function=tmdb.Movies(tmdb_id).info, cache_duration=GET_BY_TMDB_ID_CACHE_TIMEOUT, kwargs={"append_to_response": extras}, ), ) # Obtain a TV show by ID if content_type == "tv": return self._set_content_attributes( content_type, cache.handler( "get tv by tmdb id", page_key=tmdb_id, function=tmdb.TV(tmdb_id).info, cache_duration=GET_BY_TMDB_ID_CACHE_TIMEOUT, kwargs={"append_to_response": extras}, ), ) # Content Type was invalid log.handler( "Invalid content_type " + str(content_type) + " in get_by_id().", log.WARNING, _logger, ) except: log.handler( "Failed to obtain content by ID!", log.ERROR, _logger, ) return None
def sonarr_quality_profiles(self): """Returns the quality profiles available within Sonarr""" try: return self.__sonarr.get_quality_profiles() except: log.handler( "Failed to get sonarr quality profiles!", log.ERROR, _logger, )
def radarr_quality_profiles(self): """Returns the quality profiles available within Radarr""" try: return self.__radarr.getQualityProfiles() except: log.handler( "Failed to get radarr quality profiles!", log.ERROR, _logger, )
def radarr_root_dirs(self): """Returns the root dirs available within Radarr""" try: return self.__radarr.getRoot() except: log.handler( "Failed to get radarr root dirs!", log.ERROR, _logger, )
def __recommended(self, tmdb_id, content_type, page_number): """Obtains recommendations given a TMDB ID. Args: id: An Integer or String containing the TMDB ID. content_type: String containing "movie" or "tv". page_number: An Integer that is the page number to return. """ try: if content_type == "movie": return self._set_content_attributes( content_type, cache.handler( "discover recommended movies", page_key=str(tmdb_id) + "page" + str(page_number), function=tmdb.Movies(tmdb_id).recommendations, cache_duration=RECOMMENDED_CACHE_TIMEOUT, kwargs={ "language": LANGUAGE, "page": page_number, }, ), ) if content_type == "tv": return self._set_content_attributes( content_type, cache.handler( "discover recommended tv", page_key=str(tmdb_id) + "page" + str(page_number), function=tmdb.TV(tmdb_id).recommendations, cache_duration=RECOMMENDED_CACHE_TIMEOUT, kwargs={ "language": LANGUAGE, "page": page_number, }, ), ) # Content Type was invalid log.handler( "Invalid content_type " + str(content_type) + " in recommend().", log.WARNING, _logger, ) except: log.handler( "Failed to obtain recommendations!", log.ERROR, _logger, ) return None
async def receive_json(self, content, **kwargs): """When the browser attempts to send a message to the server.""" log.handler( content, log.INFO, _logger, ) # Reject users that aren't logged in if (isinstance(self.scope["user"], AnonymousUser) or not self.scope["user"].is_authenticated): await self.__forbidden()
def sonarr_root_dirs(self): """Returns the root dirs available within Sonarr""" try: return self.__sonarr.get_root_folder() except: log.handler( "Failed to get sonarr root dirs!", log.ERROR, _logger, )
def __set_original_rank(self, results): """Sets the search ranking was provided by Sonarr/Radarr""" try: for index, result in enumerate(results, start=1): self.__generate_original_rank(result, index) # Sort them by the similarity metric return results except: log.handler("Failed to rank results", log.ERROR, _logger) return results
def _shuffle_results(query): """Shuffle API results""" try: shuffle(query["results"]) return query except: log.handler( "Failed to shuffle results!", log.ERROR, _logger, )
def delete(self, **kwargs): """Deletes an existing movie, series, or episode(s) using Sonarr or Radarr. Kwargs: # Pick One ID radarr_id: An integer string containing the Radarr ID. sonarr_id: An integer containing the Sonarr ID. episode_file_ids: A list of integers containing the specific "episodeFileId" values. Returns: 1) JSON response of removing the content. 2) None """ # TODO: Need to remove any currently downloading content for things that are removed. # TODO: Need to blacklist deleted content. try: # Remove a movie with a specific Radarr ID. if kwargs.__contains__("radarr_id"): return self.__radarr.delMovie(kwargs["radarr_id"], delFiles=True) # Remove a show with a specific Sonarr ID. if kwargs.__contains__("sonarr_id"): # Remove the whole show return self.__sonarr.delSeries(kwargs["sonarr_id"], delFiles=True) # Remove episodes with Sonarr episode IDs. if kwargs.__contains__("episode_file_ids"): response = [] # Remove all episode files in the list for episode_id in kwargs["episode_file_ids"]: response.append( self.__sonarr.del_episode_file_by_episode_id( episode_id)) return response # Invalid parameter log.handler( "Invalid parameter for deleting content!", log.WARNING, self.__logger, ) return None except: log.handler( "Failed to delete content!", log.ERROR, self.__logger, ) return None
def __generate_request_card(entry, content_discovery, content_manager): """Generates a single request card. This is intended for multi-threaded use.""" card = None # Fetch TMDB entry if entry["source"] == "tmdb": card = content_discovery.get_by_tmdb_id( tmdb_id=entry["content_id"], content_type=entry["content_type"], obtain_extras=False, ) # Fetch TVDB entry elif entry["source"] == "tvdb": # Attempt to convert card to TMDB conversion = content_discovery.get_by_tvdb_id(tvdb_id=entry["content_id"]) # Conversion found if conversion.__contains__("tv_results") and conversion["tv_results"]: card = conversion["tv_results"][0] # Convert all requests to use this new ID old_requests = UserRequest.objects.filter( content_id=entry["content_id"], source="tvdb" ) old_requests.update(content_id=card["id"], source="tmdb") # Fallback to checking sonarr's database else: card = content_manager.get(tvdb_id=entry["content_id"]) # Determine if the card has a known poster image if isinstance(card, dict): card["content_type"] = entry["content_type"] if card.__contains__("images"): remote_poster = is_key_value_in_list( "coverType", "poster", card["images"], return_item=True ) if remote_poster: card["remotePoster"] = remote_poster["remoteUrl"] if card is None: log.handler( entry["content_type"] + " from " + entry["source"] + " with ID " + entry["content_id"] + " no longer exists!", log.WARNING, _logger, ) return card
def discover_by_filter(self, content_type, **kwargs): """Filter by keywords or any other TMDB filter capable arguements. (see tmdbsimple discover.movie and discover.tv) Args: content_type: String containing "movie" or "tv". # Additional kwargs # keyword: A single String or a List of strings. _______: Any other values supported by tmdbsimple discover.movie or discover.tv. """ try: if kwargs.__contains__("keyword"): # Convert all keywords to IDs keyword_ids = self.__keywords_to_ids(kwargs["keyword"]) if keyword_ids is not None: kwargs["with_keywords"] = keyword_ids # Remove keyword strings (invalid parameters) kwargs.__delitem__("keyword") # Perform a discovery search for a movie if content_type.lower() == "movie": return cache.handler( "discover movie cache", function=tmdb.Discover().movie, page_key=str(kwargs), cache_duration=DISCOVER_BY_FILTER_CACHE_TIMEOUT, **kwargs, ) # Perform a discovery search for a TV show if content_type.lower() == "tv": return cache.handler( "discover tv cache", function=tmdb.Discover().tv, page_key=str(kwargs), cache_duration=DISCOVER_BY_FILTER_CACHE_TIMEOUT, **kwargs, ) # Content Type was invalid log.handler( "Invalid content_type " + str(content_type) + " in discover().", log.WARNING, self.__logger, ) return {} except: log.handler("Failed to discover!", log.ERROR, self.__logger) return {}
def __round(self, number, significant_figures=5): # Round a number using the concept of significant figures try: # Rounding would've returned an error if the number is 0 if number == 0: return number # Round a non-zero number return round( number, -int(floor(log10(number))) + (significant_figures - 1)) except: log.handler("Failed to round!", log.ERROR, self.__logger) return number