diff --git a/.gitignore b/.gitignore index 2c40804..4c20ccf 100644 --- a/.gitignore +++ b/.gitignore @@ -31,5 +31,6 @@ __pycache__/ # PyInstaller build/ +dist/ *.manifest *.spec \ No newline at end of file diff --git a/README.md b/README.md index 2aa10cd..2f8292d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Traktarr -Script to add new TV series & movies to Sonarr/Radarr based on Trakt lists. +Script to add new shows & movies to Sonarr/Radarr based on Trakt lists. # Requirements 1. Python 3.5 or higher (`sudo apt install python3 python3-pip`). @@ -127,21 +127,21 @@ Options: ``` -## TV Shows +## Shows ``` Usage: python3 traktarr.py shows [OPTIONS] - Add new series to Sonarr. + Add new shows to Sonarr. Options: -t, --list-type [anticipated|trending|popular] Trakt list to process. [required] - -l, --add-limit INTEGER Limit number of series added to Sonarr. + -l, --add-limit INTEGER Limit number of shows added to Sonarr. [default: 0] -d, --add-delay FLOAT Seconds between each add request to Sonarr. [default: 2.5] - --no-search Disable search when adding series to Sonarr. + --no-search Disable search when adding shows to Sonarr. --help Show this message and exit. ``` diff --git a/media/sonarr.py b/media/sonarr.py index 8b14abc..7030f2f 100644 --- a/media/sonarr.py +++ b/media/sonarr.py @@ -46,12 +46,12 @@ class Sonarr: if req.status_code == 200: resp_json = req.json() - log.debug("Found %d series", len(resp_json)) + log.debug("Found %d shows", len(resp_json)) return resp_json else: - log.error("Failed to retrieve all series, request response: %d", req.status_code) + log.error("Failed to retrieve all shows, request response: %d", req.status_code) except Exception: - log.exception("Exception retrieving series: ") + log.exception("Exception retrieving show: ") return None @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) @@ -64,6 +64,7 @@ class Sonarr: if req.status_code == 200: resp_json = req.json() + log.debug("Found %d quality profiles", len(resp_json)) for profile in resp_json: if profile['name'].lower() == profile_name.lower(): log.debug("Found id of %s profile: %d", profile_name, profile['id']) @@ -76,12 +77,57 @@ class Sonarr: return None @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) - def add_series(self, series_tvdbid, series_title, series_title_slug, profile_id, root_folder, search_missing=False): + def get_tag_id(self, tag_name): + try: + # make request + req = requests.get(urljoin(self.server_url, 'api/tag'), headers=self.headers, timeout=30) + log.debug("Request URL: %s", req.url) + log.debug("Request Response: %d", req.status_code) + + if req.status_code == 200: + resp_json = req.json() + log.debug("Found %d tags", len(resp_json)) + for tag in resp_json: + if tag['label'].lower() == tag_name.lower(): + log.debug("Found id of %s tag: %d", tag_name, tag['id']) + return tag['id'] + log.debug("Tag %s with id %d did not match %s", tag['label'], tag['id'], tag_name) + else: + log.error("Failed to retrieve all tags, request response: %d", req.status_code) + except Exception: + log.exception("Exception retrieving id of tag %s: ", tag_name) + return None + + @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) + def get_tags(self): + tags = {} + try: + # make request + req = requests.get(urljoin(self.server_url, 'api/tag'), headers=self.headers, timeout=30) + log.debug("Request URL: %s", req.url) + log.debug("Request Response: %d", req.status_code) + + if req.status_code == 200: + resp_json = req.json() + log.debug("Found %d tags", len(resp_json)) + for tag in resp_json: + tags[tag['label']] = tag['id'] + return tags + else: + log.error("Failed to retrieve all tags, request response: %d", req.status_code) + except Exception: + log.exception("Exception retrieving tags: ") + return None + + @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) + def add_series(self, series_tvdbid, series_title, series_title_slug, profile_id, root_folder, tag_ids=None, + search_missing=False): try: # generate payload payload = { 'tvdbId': series_tvdbid, 'title': series_title, 'titleSlug': series_title_slug, - 'qualityProfileId': profile_id, 'images': [], + 'qualityProfileId': profile_id, 'tags': [] if not tag_ids or not isinstance(tag_ids, list) else tag_ids, + 'images': [], 'seasons': [], 'seasonFolder': True, 'monitored': True, 'rootFolderPath': root_folder, 'addOptions': {'ignoreEpisodesWithFiles': False, @@ -105,5 +151,5 @@ class Sonarr: log.error("Failed to add %s (%d), unexpected response:\n%s", series_title, series_tvdbid, req.text) return False except Exception: - log.exception("Exception adding series %s (%d): ", series_title, series_tvdbid) + log.exception("Exception adding show %s (%d): ", series_title, series_tvdbid) return None diff --git a/misc/config.py b/misc/config.py index 7a59244..46f6133 100644 --- a/misc/config.py +++ b/misc/config.py @@ -16,7 +16,11 @@ base_config = { 'url': 'http://localhost:8989', 'api_key': '', 'profile': 'HD-1080p', - 'root_folder': '/tv/' + 'root_folder': '/tv/', + 'tags': { + 'amzn': ['hbo', 'amc', 'usa network', 'tnt', 'starz', 'the cw', 'fx', 'fox', 'abc', 'nbc', 'cbs', 'tbs', + 'amazon', 'syfy', 'cinemax'] + } }, 'radarr': { 'url': 'http://localhost:7878', diff --git a/misc/helpers.py b/misc/helpers.py index 991215e..c02bf97 100644 --- a/misc/helpers.py +++ b/misc/helpers.py @@ -8,17 +8,29 @@ log = logger.get_logger(__name__) # SONARR ############################################################ +def sonarr_series_tag_id_from_network(profile_tags, network_tags, network): + try: + for tag_name, tag_networks in network_tags.items(): + for tag_network in tag_networks: + if tag_network.lower() in network.lower() and tag_name.lower() in profile_tags: + log.debug("Using %s tag for network: %s", tag_name, network) + return [profile_tags[tag_name.lower()]] + except Exception: + log.exception("Exception determining tag to use for network %s: ", network) + return None + + def sonarr_series_to_tvdb_dict(sonarr_series): series = {} try: for tmp in sonarr_series: if 'tvdbId' not in tmp: - log.debug("Could not handle series: %s", tmp['title']) + log.debug("Could not handle show: %s", tmp['title']) continue series[tmp['tvdbId']] = tmp return series except Exception: - log.exception("Exception processing sonarr series to tvdb dict: ") + log.exception("Exception processing Sonarr shows to TVDB dict: ") return None @@ -47,11 +59,11 @@ def sonarr_remove_existing_series(sonarr_series, trakt_series): new_series_list.append(tmp) - log.debug("Filtered %d trakt shows to %d shows that weren't already in Sonarr", len(trakt_series), + log.debug("Filtered %d Trakt shows to %d shows that weren't already in Sonarr", len(trakt_series), len(new_series_list)) return new_series_list except Exception: - log.exception("Exception removing existing series from trakt list: ") + log.exception("Exception removing existing shows from Trakt list: ") return None @@ -133,7 +145,7 @@ def trakt_blacklisted_show_runtime(show, lowest_runtime): blacklisted = True elif int(show['show']['runtime']) < lowest_runtime: log.debug("%s was blacklisted because it had a runtime of: %d", show['show']['title'], - show['movie']['runtime']) + show['show']['runtime']) blacklisted = True except Exception: @@ -173,7 +185,7 @@ def radarr_movies_to_tmdb_dict(radarr_movies): movies[tmp['tmdbId']] = tmp return movies except Exception: - log.exception("Exception processing radarr movies to tmdb dict: ") + log.exception("Exception processing Radarr movies to TMDB dict: ") return None @@ -202,11 +214,11 @@ def radarr_remove_existing_movies(radarr_movies, trakt_movies): new_movies_list.append(tmp) - log.debug("Filtered %d trakt movies to %d movies that weren't already in Radarr", len(trakt_movies), + log.debug("Filtered %d Trakt movies to %d movies that weren't already in Radarr", len(trakt_movies), len(new_movies_list)) return new_movies_list except Exception: - log.exception("Exception removing existing movies from trakt list: ") + log.exception("Exception removing existing movies from Trakt list: ") return None diff --git a/traktarr.py b/traktarr.py index 5ef4e34..92afffd 100644 --- a/traktarr.py +++ b/traktarr.py @@ -19,7 +19,7 @@ log = logger.get_logger('traktarr') # Click -@click.group(help='Add new series/movies to Sonarr & Radarr from Trakt.') +@click.group(help='Add new shows & movies to Sonarr/Radarr from Trakt lists.') def app(): pass @@ -28,12 +28,12 @@ def app(): # SHOWS ############################################################ -@app.command(help='Add new series to Sonarr.') +@app.command(help='Add new shows to Sonarr.') @click.option('--list-type', '-t', type=click.Choice(['anticipated', 'trending', 'popular']), help='Trakt list to process.', required=True) -@click.option('--add-limit', '-l', default=0, help='Limit number of series added to Sonarr.', show_default=True) +@click.option('--add-limit', '-l', default=0, help='Limit number of shows added to Sonarr.', show_default=True) @click.option('--add-delay', '-d', default=2.5, help='Seconds between each add request to Sonarr.', show_default=True) -@click.option('--no-search', is_flag=True, help='Disable search when adding series to Sonarr.') +@click.option('--no-search', is_flag=True, help='Disable search when adding shows to Sonarr.') def shows(list_type, add_limit=0, add_delay=2.5, no_search=False): added_shows = 0 @@ -61,13 +61,21 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False): else: log.info("Retrieved Profile ID for %s: %d", cfg.sonarr.profile, profile_id) + # retrieve profile tags + profile_tags = sonarr.get_tags() + if profile_tags is None: + log.error("Aborting due to failure to retrieve Tag ID's") + return + else: + log.info("Retrieved %d Tag ID's", len(profile_tags)) + # get sonarr series list sonarr_series_list = sonarr.get_series() if not sonarr_series_list: - log.error("Aborting due to failure to retrieve Sonarr series list") + log.error("Aborting due to failure to retrieve Sonarr shows list") return else: - log.info("Retrieved Sonarr series list, series found: %d", len(sonarr_series_list)) + log.info("Retrieved Sonarr shows list, shows found: %d", len(sonarr_series_list)) # get trakt series list trakt_series_list = None @@ -81,23 +89,23 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False): log.error("Aborting due to unknown Trakt list type") return if not trakt_series_list: - log.error("Aborting due to failure to retrieve Trakt %s series list", list_type) + log.error("Aborting due to failure to retrieve Trakt %s shows list", list_type) return else: - log.info("Retrieved Trakt %s series list, series found: %d", list_type, len(trakt_series_list)) + log.info("Retrieved Trakt %s shows list, shows found: %d", list_type, len(trakt_series_list)) # build filtered series list without series that exist in sonarr processed_series_list = helpers.sonarr_remove_existing_series(sonarr_series_list, trakt_series_list) if not processed_series_list: - log.error("Aborting due to failure to remove existing Sonarr series from retrieved Trakt series list") + log.error("Aborting due to failure to remove existing Sonarr shows from retrieved Trakt shows list") return else: - log.info("Removed existing Sonarr series from Trakt series list, series left to process: %d", + log.info("Removed existing Sonarr shows from Trakt shows list, shows left to process: %d", len(processed_series_list)) # sort filtered series list by highest votes sorted_series_list = sorted(processed_series_list, key=lambda k: k['show']['votes'], reverse=True) - log.info("Sorted series list to process by highest votes") + log.info("Sorted shows list to process by highest votes") # loop series_list log.info("Processing list now...") @@ -109,13 +117,18 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False): ', '.join(series['show']['genres']), series['show']['network'], series['show']['country'].upper()) + # determine which tags to use when adding this series + use_tags = helpers.sonarr_series_tag_id_from_network(profile_tags, cfg.sonarr.tags, + series['show']['network']) # add show to sonarr if sonarr.add_series(series['show']['ids']['tvdb'], series['show']['title'], - series['show']['ids']['slug'], profile_id, cfg.sonarr.root_folder, not no_search): - log.info("ADDED %s (%d)", series['show']['title'], series['show']['year']) + series['show']['ids']['slug'], profile_id, cfg.sonarr.root_folder, use_tags, + not no_search): + log.info("ADDED %s (%d) with tags: %s", series['show']['title'], series['show']['year'], use_tags) added_shows += 1 else: - log.error("FAILED adding %s (%d)", series['show']['title'], series['show']['year']) + log.error("FAILED adding %s (%d) with tags: %s", series['show']['title'], series['show']['year'], + use_tags) # stop adding shows, if added_shows >= add_limit if add_limit and added_shows >= add_limit: @@ -125,9 +138,9 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False): time.sleep(add_delay) except Exception: - log.exception("Exception while processing series %s: ", series['show']['title']) + log.exception("Exception while processing show %s: ", series['show']['title']) - log.info("Added %d new shows to Sonarr", added_shows) + log.info("Added %d new show(s) to Sonarr", added_shows) @app.command(help='Add new movies to Radarr.') @@ -227,7 +240,7 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False): except Exception: log.exception("Exception while processing movie %s: ", movie['movie']['title']) - log.info("Added %d new movies to Radarr", added_movies) + log.info("Added %d new movie(s) to Radarr", added_movies) ############################################################