From eab500d57e4147498f27891707de44a9b18cb176 Mon Sep 17 00:00:00 2001 From: desimaniac Date: Tue, 8 May 2018 11:49:32 -0500 Subject: [PATCH 01/15] Readme: minor fixes --- README.md | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 255f313..e94a805 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Types of Trakt lists supported: - Anticipated - - boxoffice + - Boxoffice - Public lists @@ -26,10 +26,10 @@ Types of Trakt lists supported: # Demo +Click to enlarge. [![asciicast](assets/demo.gif)](https://asciinema.org/a/180044) -Click to enlarge. # Requirements @@ -48,10 +48,10 @@ Install traktarr to be run with `traktarr` command. 4. `cd traktarr` 5. `sudo python3 -m pip install -r requirements.txt` 6. `sudo ln -s /opt/traktarr/traktarr.py /usr/local/bin/traktarr` -7. `traktarr` - run once to generate a default a config.json file. +7. `traktarr` - run once to generate a sample a config.json file. 8. `nano config.json` - edit preferences. -## 2. Create app authentication +## 2. Create a Trakt Application 1. Create a Trakt application by going [here](https://trakt.tv/oauth/applications/new) 2. Enter a name for your application; for example `traktarr` @@ -72,7 +72,6 @@ Install traktarr to be run with `traktarr` command. For each user you want to access the private lists for (i.e. watchlist and/or custom lists), you will need to to authenticate that user. - Repeat the following steps for every user you want to authenticate: 1. Run `traktarr trakt_authentication` 2. You wil get the following prompt: @@ -188,7 +187,7 @@ You can repeat this process for as many users as you like. "service": "slack", "webhook_url": "" }, - "verbose": true, + "verbose": true }, "radarr": { "api_key": "", @@ -392,7 +391,7 @@ Use filters to specify the movie/shows's country of origin or blacklist (i.e. fi `blacklist_title_keywords` - blacklist certain words in titles. -`blacklisted_genres` - blacklist certain generes. +`blacklisted_genres` - blacklist certain genres. `blacklisted_max_year` - blacklist release dates after specified year. @@ -448,7 +447,7 @@ Use filters to specify the movie/shows's country of origin or blacklist (i.e. fi `allowed_countries` - allowed countries of origin. -`blacklisted_genres` - blacklist certain generes. +`blacklisted_genres` - blacklist certain genres. `blacklisted_max_year` - blacklist release dates after specified year. @@ -463,9 +462,11 @@ Use filters to specify the movie/shows's country of origin or blacklist (i.e. fi ## Notifications -Notification alerts during tasks. +Notification alerts for traktarr tasks. -Currently, only Pushover and Slack are supported. More will abe added later. +_Note: Manual commands need the `--notifications` flag._ + +Currently, only Pushover and Slack are supported. More will be added later. ```json @@ -479,7 +480,7 @@ Currently, only Pushover and Slack are supported. More will abe added later. "service": "slack", "webhook_url": "" }, - "verbose": true, + "verbose": true }, ``` @@ -668,7 +669,7 @@ Options: --help Show this message and exit. ``` -_Note: `-id` and `--movie_id` support both Trakt IDs and IMDB IDs._ +_Note: This command only works with `-id` or `--show_id` specified (i.e. not with lists), and supports both Trakt IDs and IMDB IDs._ ### Movies (Multiple Movies) @@ -719,7 +720,8 @@ Options: --help Show this message and exit. ``` -_Note: `-id` and `--show_id` support both Trakt IDs and IMDB IDs._ +_Note: This command only works with `-id` or `--show_id` specified (i.e. not with lists), and supports both Trakt IDs and IMDB IDs._ + ### Shows (Multiple Shows) From 994dee7a2689307191d23772b5ccc7f1e2d01e0c Mon Sep 17 00:00:00 2001 From: Filipe Santos Date: Sat, 12 May 2018 15:52:20 +1200 Subject: [PATCH 02/15] Flag to do a first run --- traktarr.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/traktarr.py b/traktarr.py index 284bc31..68dd02d 100755 --- a/traktarr.py +++ b/traktarr.py @@ -705,28 +705,34 @@ def automatic_movies(add_delay=2.5, no_search=False, notifications=False): @click.option('--add-delay', '-d', default=2.5, help='Seconds between each add request to Sonarr / Radarr.', show_default=True) @click.option('--no-search', is_flag=True, help='Disable search when adding to Sonarr / Radarr.') +@click.option('--run-now', is_flag=True, help="Do a first run immediately without waiting.") @click.option('--no-notifications', is_flag=True, help="Disable notifications.") -def run(add_delay=2.5, no_search=False, no_notifications=False): +def run(add_delay=2.5, no_search=False, run_now=False, no_notifications=False): log.info("Automatic mode is now running...") - # Add tasks to schedule and do first run + # Add tasks to schedule and do first run if enabled if cfg.automatic.movies.interval: - schedule.every(cfg.automatic.movies.interval).hours.do( + movie_schedule = schedule.every(cfg.automatic.movies.interval).hours.do( automatic_movies, add_delay, no_search, not no_notifications - ).run() + ) + if run_now: + movie_schedule.run() + # Sleep between tasks time.sleep(add_delay) if cfg.automatic.shows.interval: - schedule.every(cfg.automatic.shows.interval).hours.do( + shows_schedule = schedule.every(cfg.automatic.shows.interval).hours.do( automatic_shows, add_delay, no_search, not no_notifications - ).run() + ) + if run_now: + shows_schedule.run() # Enter running schedule while True: From 405c5b177684fc262d1f37b0c772c1ab328e6fe8 Mon Sep 17 00:00:00 2001 From: l3uddz <> Date: Sat, 12 May 2018 10:15:45 +0100 Subject: [PATCH 03/15] ignore venv folder --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 45b27b2..b57a0c5 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,9 @@ __pycache__/ # Pyenv **/.python-version +# Venv +venv/ + # PyInstaller build/ dist/ From 02c2b5ee05e986ee3891a63a271af9bc64c391db Mon Sep 17 00:00:00 2001 From: desimaniac Date: Sun, 13 May 2018 15:56:23 -0500 Subject: [PATCH 04/15] Readme: Added TOC, Shields, and misc other tweaks. --- README.md | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 86 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e94a805..eed708c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,38 @@ + + +- [traktarr](#traktarr) +- [Demo](#demo) +- [Requirements](#requirements) +- [Installation](#installation) + - [1. Base Install](#1-base-install) + - [2. Create a Trakt Application](#2-create-a-trakt-application) + - [3. Authenticate User(s) (optional)](#3-authenticate-users-optional) +- [Configuration](#configuration) + - [Sample Configuration](#sample-configuration) + - [Core](#core) + - [Automatic](#automatic) + - [Filters](#filters) + - [Notifications](#notifications) + - [Radarr](#radarr) + - [Sonarr](#sonarr) + - [Trakt](#trakt) +- [Usage](#usage) + - [Automatic (Scheduled)](#automatic-scheduled) + - [Manual (CLI)](#manual-cli) + - [Examples (Manual)](#examples-manual) + + + +--- + + [![made-with-python](https://img.shields.io/badge/Made%20with-Python-blue.svg)](https://www.python.org/) [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://github.com/l3uddz/traktarr/blob/master/LICENSE) +[![Feature Requests](https://img.shields.io/badge/Requests-Feathub-blue.svg)](http://feathub.com/l3uddz/traktarr) +[![Discord](https://img.shields.io/discord/381077432285003776.svg)](https://discord.gg/xmNYmSJ) + + # traktarr -Add new shows & movies to Sonarr/Radarr from Trakt. + +traktarr uses trakt to add new shows into Sonarr and new movies into Radarr. Types of Trakt lists supported: @@ -33,8 +66,11 @@ Click to enlarge. # Requirements -1. Python 3.5 or higher (`sudo apt install python3 python3-pip`). -2. requirements.txt modules (see below). +1. Ubuntu/Debian + +2. Python 3.5 or higher (`sudo apt install python3 python3-pip`). + +3. requirements.txt modules (see below). # Installation @@ -43,20 +79,31 @@ Click to enlarge. Install traktarr to be run with `traktarr` command. 1. `cd /opt` + 2. `sudo git clone https://github.com/l3uddz/traktarr` + 3. `sudo chown -R user:group traktarr` (run `id` to find your user / group) + 4. `cd traktarr` + 5. `sudo python3 -m pip install -r requirements.txt` + 6. `sudo ln -s /opt/traktarr/traktarr.py /usr/local/bin/traktarr` + 7. `traktarr` - run once to generate a sample a config.json file. + 8. `nano config.json` - edit preferences. ## 2. Create a Trakt Application 1. Create a Trakt application by going [here](https://trakt.tv/oauth/applications/new) + 2. Enter a name for your application; for example `traktarr` + 3. Enter `urn:ietf:wg:oauth:2.0:oob` in the `Redirect uri` field. + 4. Click "SAVE APP". + 5. Open the traktarr configuration file `config.json` and insert the Client ID in the `client_id` and the Client Secret in the `client_secret`, like this: ``` @@ -73,7 +120,9 @@ Install traktarr to be run with `traktarr` command. For each user you want to access the private lists for (i.e. watchlist and/or custom lists), you will need to to authenticate that user. Repeat the following steps for every user you want to authenticate: + 1. Run `traktarr trakt_authentication` + 2. You wil get the following prompt: ``` @@ -81,13 +130,19 @@ Repeat the following steps for every user you want to authenticate: - Go to: https://trakt.tv/activate on any device and enter A0XXXXXX. We'll be polling Trakt every 5 seconds for a reply ``` 3. Go to https://trakt.tv/activate. + 4. Enter the code you see in your terminal. + 5. Click continue. + 6. If you are not logged in to Trakt, login now. + 7. Click "Accept". + 8. You will get the message: "Woohoo! Your device is now connected and will automatically refresh in a few seconds.". You've now authenticated the user. + You can repeat this process for as many users as you like. @@ -615,14 +670,41 @@ Trakt Authentication info: ## Automatic (Scheduled) -To have traktarr get Movies and Shows for you automatically, on set interval. +### Setup + +To have traktarr get Movies and Shows for you automatically, on set interval, do the following: 1. `sudo cp /opt/traktarr/systemd/traktarr.service /etc/systemd/system/` + 2. `sudo nano /etc/systemd/system/traktarr.service` and edit user/group to match yours. + 3. `sudo systemctl daemon-reload` + 4. `sudo systemctl enable traktarr.service` + 5. `sudo systemctl start traktarr.service` +### Customize + +You can customize how the scheduled traktarr is ran by editing the `traktarr.service` file and adding any of the following options: + +``` + -d, --add-delay FLOAT Seconds between each add request to Sonarr / Radarr. + [default: 2.5] + --no-search Disable search when adding to Sonarr / Radarr. + --run-now Do a first run immediately without waiting. + --no-notifications Disable notifications. + --help Show this message and exit. +``` + +You can bring up the list, anytime, by running the following command: + +``` +traktarr run --help +``` + +\* Remember, other configuration options need to go into the `config.json` file under the `Automatic` section. + ## Manual (CLI) ### General From b88d9aadadcc1c44670f72970ed3c102599ea19b Mon Sep 17 00:00:00 2001 From: desimaniac Date: Sun, 13 May 2018 15:57:09 -0500 Subject: [PATCH 05/15] Readme: minor fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eed708c..a14dd9d 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ # traktarr -traktarr uses trakt to add new shows into Sonarr and new movies into Radarr. +traktarr uses Trakt to add new shows into Sonarr and new movies into Radarr. Types of Trakt lists supported: From a7ae649de1c7148fdd69a57f6b1234e27f041c54 Mon Sep 17 00:00:00 2001 From: desimaniac Date: Sun, 13 May 2018 15:59:53 -0500 Subject: [PATCH 06/15] Readme: Bolden name for clarifty --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a14dd9d..08d8801 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ # traktarr -traktarr uses Trakt to add new shows into Sonarr and new movies into Radarr. +**traktarr** uses Trakt to add new shows into Sonarr and new movies into Radarr. Types of Trakt lists supported: From 4b169f58a56ed83e2c8699b0522174c4ee2298f9 Mon Sep 17 00:00:00 2001 From: Mitchell Klijs Date: Thu, 3 May 2018 10:13:04 +0200 Subject: [PATCH 07/15] Refactor Sonarr and Radarr classes to share common functionality --- media/pvr.py | 141 ++++++++++++++++++++++++++++++++++++++++ media/radarr.py | 145 +++++------------------------------------- media/sonarr.py | 166 +++++------------------------------------------- media/trakt.py | 7 +- misc/helpers.py | 18 ++++++ traktarr.py | 4 +- 6 files changed, 193 insertions(+), 288 deletions(-) create mode 100644 media/pvr.py diff --git a/media/pvr.py b/media/pvr.py new file mode 100644 index 0000000..8221ae7 --- /dev/null +++ b/media/pvr.py @@ -0,0 +1,141 @@ +import os.path +from abc import ABC, abstractmethod + +import backoff +import requests + +from misc import helpers +from misc import str as misc_str +from misc.helpers import backoff_handler +from misc.log import logger + +log = logger.get_logger(__name__) + + +class PVR(ABC): + def __init__(self, server_url, api_key): + self.server_url = server_url + self.api_key = api_key + self.headers = { + 'Content-Type': 'application/json', + 'X-Api-Key': self.api_key, + } + + def validate_api_key(self): + try: + # request system status to validate api_key + req = requests.get( + os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/system/status'), + headers=self.headers, + timeout=60 + ) + log.debug("Request Response: %d", req.status_code) + + if req.status_code == 200 and 'version' in req.json(): + return True + return False + except Exception: + log.exception("Exception validating api_key: ") + return False + + @abstractmethod + def get_objects(self): + pass + + @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) + def _get_objects(self, endpoint): + try: + # make request + req = requests.get( + os.path.join(misc_str.ensure_endswith(self.server_url, "/"), endpoint), + headers=self.headers, + timeout=60 + ) + 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 objects", len(resp_json)) + return resp_json + else: + log.error("Failed to retrieve all objects, request response: %d", req.status_code) + except Exception: + log.exception("Exception retrieving objects: ") + return None + + @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) + def get_profile_id(self, profile_name): + try: + # make request + req = requests.get( + os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/profile'), + headers=self.headers, + timeout=60 + ) + log.debug("Request URL: %s", req.url) + log.debug("Request Response: %d", req.status_code) + + if req.status_code == 200: + resp_json = req.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']) + return profile['id'] + log.debug("Profile %s with id %d did not match %s", profile['name'], profile['id'], profile_name) + else: + log.error("Failed to retrieve all quality profiles, request response: %d", req.status_code) + except Exception: + log.exception("Exception retrieving id of profile %s: ", profile_name) + return None + + def _prepare_add_object_payload(self, title, title_slug, profile_id, root_folder): + return { + 'title': title, + 'titleSlug': title_slug, + 'qualityProfileId': profile_id, + 'images': [], + 'monitored': True, + 'rootFolderPath': root_folder, + 'addOptions': { + 'ignoreEpisodesWithFiles': False, + 'ignoreEpisodesWithoutFiles': False, + } + } + + @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) + def _add_object(self, endpoint, payload, identifier_field, identifier): + try: + # make request + req = requests.post( + os.path.join(misc_str.ensure_endswith(self.server_url, "/"), endpoint), + headers=self.headers, + json=payload, + timeout=60 + ) + log.debug("Request URL: %s", req.url) + log.debug("Request Payload: %s", payload) + log.debug("Request Response Code: %d", req.status_code) + log.debug("Request Response Text:\n%s", req.text) + + response_json = None + if 'json' in req.headers['Content-Type'].lower(): + response_json = helpers.get_response_dict(req.json(), identifier_field, identifier) + + if (req.status_code == 201 or req.status_code == 200) \ + and (response_json and identifier_field in response_json) \ + and response_json[identifier_field] == identifier: + log.debug("Successfully added %s (%d)", payload['title'], identifier) + return True + elif response_json and ('errorMessage' in response_json or 'message' in response_json): + message = response_json['errorMessage'] if 'errorMessage' in response_json else response_json['message'] + + log.error("Failed to add %s (%d) - status_code: %d, reason: %s", payload['title'], identifier, + req.status_code, message) + return False + else: + log.error("Failed to add %s (%d), unexpected response:\n%s", payload['title'], identifier, req.text) + return False + except Exception: + log.exception("Exception adding %s (%d): ", payload['title'], identifier) + return None diff --git a/media/radarr.py b/media/radarr.py index 981af7e..5bb61fa 100644 --- a/media/radarr.py +++ b/media/radarr.py @@ -1,143 +1,28 @@ -import os.path - import backoff -import requests -from misc import helpers -from misc import str as misc_str +from media.pvr import PVR +from misc.helpers import backoff_handler, dict_merge from misc.log import logger log = logger.get_logger(__name__) -def backoff_handler(details): - log.warning("Backing off {wait:0.1f} seconds afters {tries} tries " - "calling function {target} with args {args} and kwargs " - "{kwargs}".format(**details)) - - -class Radarr: - def __init__(self, server_url, api_key): - self.server_url = server_url - self.api_key = api_key - self.headers = { - 'Content-Type': 'application/json', - 'X-Api-Key': self.api_key, - } - - def validate_api_key(self): - try: - # request system status to validate api_key - req = requests.get( - os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/system/status'), - headers=self.headers, - timeout=60 - ) - log.debug("Request Response: %d", req.status_code) - - if req.status_code == 200 and 'version' in req.json(): - return True - return False - except Exception: - log.exception("Exception validating api_key: ") - return False - - @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) - def get_movies(self): - try: - # make request - req = requests.get( - os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/movie'), - headers=self.headers, - timeout=60 - ) - 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 movies", len(resp_json)) - return resp_json - else: - log.error("Failed to retrieve all movies, request response: %d", req.status_code) - except Exception: - log.exception("Exception retrieving movies: ") - return None - - @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) - def get_profile_id(self, profile_name): - try: - # make request - req = requests.get( - os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/profile'), - headers=self.headers, - timeout=60 - ) - log.debug("Request URL: %s", req.url) - log.debug("Request Response: %d", req.status_code) - - if req.status_code == 200: - resp_json = req.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']) - return profile['id'] - log.debug("Profile %s with id %d did not match %s", profile['name'], profile['id'], profile_name) - else: - log.error("Failed to retrieve all quality profiles, request response: %d", req.status_code) - except Exception: - log.exception("Exception retrieving id of profile %s: ", profile_name) - return None +class Radarr(PVR): + def get_objects(self): + return self._get_objects('api/movie') @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) def add_movie(self, movie_tmdbid, movie_title, movie_year, movie_title_slug, profile_id, root_folder, search_missing=False): - try: - # generate payload - payload = { - 'tmdbId': movie_tmdbid, - 'title': movie_title, - 'year': movie_year, - 'qualityProfileId': profile_id, - 'images': [], - 'monitored': True, - 'rootFolderPath': root_folder, - 'minimumAvailability': 'released', - 'titleSlug': movie_title_slug, - 'addOptions': { - 'ignoreEpisodesWithFiles': False, - 'ignoreEpisodesWithoutFiles': False, - 'searchForMovie': search_missing - } + payload = self._prepare_add_object_payload(movie_title, movie_title_slug, profile_id, root_folder) + + payload = dict_merge(payload, { + 'tmdbId': movie_tmdbid, + 'year': movie_year, + 'minimumAvailability': 'released', + 'addOptions': { + 'searchForMovie': search_missing } + }) - # make request - req = requests.post( - os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/movie'), - headers=self.headers, - json=payload, - timeout=60 - ) - log.debug("Request URL: %s", req.url) - log.debug("Request Payload: %s", payload) - log.debug("Request Response Code: %d", req.status_code) - log.debug("Request Response Text:\n%s", req.text) - - response_json = None - if 'json' in req.headers['Content-Type'].lower(): - response_json = helpers.get_response_dict(req.json(), 'tmdbId', movie_tmdbid) - - if (req.status_code == 201 or req.status_code == 200) and (response_json and 'tmdbId' in response_json) \ - and response_json['tmdbId'] == movie_tmdbid: - log.debug("Successfully added %s (%d)", movie_title, movie_tmdbid) - return True - elif response_json and 'message' in response_json: - log.error("Failed to add %s (%d) - status_code: %d, reason: %s", movie_title, movie_tmdbid, - req.status_code, response_json['message']) - return False - else: - log.error("Failed to add %s (%d), unexpected response:\n%s", movie_title, movie_tmdbid, req.text) - return False - except Exception: - log.exception("Exception adding movie %s (%d): ", movie_title, movie_tmdbid) - return None + return self._add_object('api/movie', payload, identifier_field='tmdbId', identifier=movie_tmdbid) diff --git a/media/sonarr.py b/media/sonarr.py index 6caa082..6f287f9 100644 --- a/media/sonarr.py +++ b/media/sonarr.py @@ -3,114 +3,17 @@ import os.path import backoff import requests -from misc import helpers +from media.pvr import PVR from misc import str as misc_str +from misc.helpers import backoff_handler, dict_merge from misc.log import logger log = logger.get_logger(__name__) -def backoff_handler(details): - log.warning("Backing off {wait:0.1f} seconds afters {tries} tries " - "calling function {target} with args {args} and kwargs " - "{kwargs}".format(**details)) - - -class Sonarr: - def __init__(self, server_url, api_key): - self.server_url = server_url - self.api_key = api_key - self.headers = { - 'X-Api-Key': self.api_key, - } - - def validate_api_key(self): - try: - # request system status to validate api_key - req = requests.get(os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/system/status'), - headers=self.headers, timeout=60) - log.debug("Request Response: %d", req.status_code) - - if req.status_code == 200 and 'version' in req.json(): - return True - return False - except Exception: - log.exception("Exception validating api_key: ") - return False - - @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) - def get_series(self): - try: - # make request - req = requests.get( - os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/series'), - headers=self.headers, - timeout=60 - ) - 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 shows", len(resp_json)) - return resp_json - else: - log.error("Failed to retrieve all shows, request response: %d", req.status_code) - except Exception: - log.exception("Exception retrieving show: ") - return None - - @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) - def get_profile_id(self, profile_name): - try: - # make request - req = requests.get( - os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/profile'), - headers=self.headers, - timeout=60 - ) - 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 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']) - return profile['id'] - log.debug("Profile %s with id %d did not match %s", profile['name'], profile['id'], profile_name) - else: - log.error("Failed to retrieve all quality profiles, request response: %d", req.status_code) - except Exception: - log.exception("Exception retrieving id of profile %s: ", profile_name) - return None - - @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) - def get_tag_id(self, tag_name): - try: - # make request - req = requests.get( - os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/tag'), - headers=self.headers, - timeout=60 - ) - 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 +class Sonarr(PVR): + def get_objects(self): + return self._get_objects('api/series') @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) def get_tags(self): @@ -140,53 +43,16 @@ class Sonarr: @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, - '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, - 'ignoreEpisodesWithoutFiles': False, - 'searchForMissingEpisodes': search_missing - } + payload = self._prepare_add_object_payload(series_title, series_title_slug, profile_id, root_folder) + + payload = dict_merge(payload, { + 'tvdbId': series_tvdbid, + 'tags': [] if not tag_ids or not isinstance(tag_ids, list) else tag_ids, + 'seasons': [], + 'seasonFolder': True, + 'addOptions': { + 'searchForMissingEpisodes': search_missing } + }) - # make request - req = requests.post( - os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/series'), - headers=self.headers, - json=payload, - timeout=60 - ) - log.debug("Request URL: %s", req.url) - log.debug("Request Payload: %s", payload) - log.debug("Request Response Code: %d", req.status_code) - log.debug("Request Response Text:\n%s", req.text) - - response_json = None - if 'json' in req.headers['Content-Type'].lower(): - response_json = helpers.get_response_dict(req.json(), 'tvdbId', series_tvdbid) - - if (req.status_code == 201 or req.status_code == 200) and (response_json and 'tvdbId' in response_json) \ - and response_json['tvdbId'] == series_tvdbid: - log.debug("Successfully added %s (%d)", series_title, series_tvdbid) - return True - elif response_json and 'errorMessage' in response_json: - log.error("Failed to add %s (%d) - status_code: %d, reason: %s", series_title, series_tvdbid, - req.status_code, response_json['errorMessage']) - return False - else: - 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 show %s (%d): ", series_title, series_tvdbid) - return None + return self._add_object('api/series', payload, identifier_field='tvdbId', identifier=series_tvdbid) diff --git a/media/trakt.py b/media/trakt.py index cb46a0e..a2c67c5 100644 --- a/media/trakt.py +++ b/media/trakt.py @@ -3,17 +3,12 @@ import time import backoff import requests +from misc.helpers import backoff_handler from misc.log import logger log = logger.get_logger(__name__) -def backoff_handler(details): - log.warning("Backing off {wait:0.1f} seconds afters {tries} tries " - "calling function {target} with args {args} and kwargs " - "{kwargs}".format(**details)) - - class Trakt: non_user_lists = ['anticipated', 'trending', 'popular', 'boxoffice'] diff --git a/misc/helpers.py b/misc/helpers.py index 60ad23e..e4f654a 100644 --- a/misc/helpers.py +++ b/misc/helpers.py @@ -410,3 +410,21 @@ def get_response_dict(response, key_field=None, key_value=None): except Exception: log.exception("Exception determining response for %s: ", response) return found_response + + +def backoff_handler(details): + log.warning("Backing off {wait:0.1f} seconds afters {tries} tries " + "calling function {target} with args {args} and kwargs " + "{kwargs}".format(**details)) + + +def dict_merge(dct, merge_dct): + for k, v in merge_dct.items(): + import collections + + if k in dct and isinstance(dct[k], dict) and isinstance(merge_dct[k], collections.Mapping): + dict_merge(dct[k], merge_dct[k]) + else: + dct[k] = merge_dct[k] + + return dct diff --git a/traktarr.py b/traktarr.py index 2c63dda..21af418 100755 --- a/traktarr.py +++ b/traktarr.py @@ -216,7 +216,7 @@ def shows(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_sea log.info("Retrieved %d Tag ID's", len(profile_tags)) # get sonarr series list - sonarr_series_list = sonarr.get_series() + sonarr_series_list = sonarr.get_objects() if not sonarr_series_list: log.error("Aborting due to failure to retrieve Sonarr shows list") if notifications: @@ -439,7 +439,7 @@ def movies(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_se log.info("Retrieved Profile ID for %s: %d", cfg.radarr.profile, profile_id) # get radarr movies list - radarr_movie_list = radarr.get_movies() + radarr_movie_list = radarr.get_objects() if not radarr_movie_list: log.error("Aborting due to failure to retrieve Radarr movies list") if notifications: From ae4d00298b396d7cb6ac3231bd266f00d1b9b096 Mon Sep 17 00:00:00 2001 From: Mitchell Klijs Date: Thu, 3 May 2018 11:56:37 +0200 Subject: [PATCH 08/15] Refactor helpers to seperate files --- helpers/misc.py | 46 +++++ helpers/radarr.py | 50 +++++ helpers/sonarr.py | 81 ++++++++ {misc => helpers}/str.py | 0 helpers/trakt.py | 249 +++++++++++++++++++++++ media/pvr.py | 8 +- media/radarr.py | 2 +- media/sonarr.py | 4 +- media/trakt.py | 2 +- misc/helpers.py | 430 --------------------------------------- traktarr.py | 28 +-- 11 files changed, 449 insertions(+), 451 deletions(-) create mode 100644 helpers/misc.py create mode 100644 helpers/radarr.py create mode 100644 helpers/sonarr.py rename {misc => helpers}/str.py (100%) create mode 100644 helpers/trakt.py delete mode 100644 misc/helpers.py diff --git a/helpers/misc.py b/helpers/misc.py new file mode 100644 index 0000000..6e0202e --- /dev/null +++ b/helpers/misc.py @@ -0,0 +1,46 @@ +from misc.log import logger + +log = logger.get_logger(__name__) + + +def get_response_dict(response, key_field=None, key_value=None): + found_response = None + try: + if isinstance(response, list): + if not key_field or not key_value: + found_response = response[0] + else: + for result in response: + if isinstance(result, dict) and key_field in result and result[key_field] == key_value: + found_response = result + break + + if not found_response: + log.error("Unable to find a result with key %s where the value is %s", key_field, key_value) + + elif isinstance(response, dict): + found_response = response + else: + log.error("Unexpected response instance type of %s for %s", type(response).__name__, response) + + except Exception: + log.exception("Exception determining response for %s: ", response) + return found_response + + +def backoff_handler(details): + log.warning("Backing off {wait:0.1f} seconds afters {tries} tries " + "calling function {target} with args {args} and kwargs " + "{kwargs}".format(**details)) + + +def dict_merge(dct, merge_dct): + for k, v in merge_dct.items(): + import collections + + if k in dct and isinstance(dct[k], dict) and isinstance(merge_dct[k], collections.Mapping): + dict_merge(dct[k], merge_dct[k]) + else: + dct[k] = merge_dct[k] + + return dct diff --git a/helpers/radarr.py b/helpers/radarr.py new file mode 100644 index 0000000..c8732f2 --- /dev/null +++ b/helpers/radarr.py @@ -0,0 +1,50 @@ +from misc.log import logger + +log = logger.get_logger(__name__) + + +def movies_to_tmdb_dict(radarr_movies): + movies = {} + try: + for tmp in radarr_movies: + if 'tmdbId' not in tmp: + log.debug("Could not handle movie: %s", tmp['title']) + continue + movies[tmp['tmdbId']] = tmp + return movies + except Exception: + log.exception("Exception processing Radarr movies to TMDB dict: ") + return None + + +def remove_existing_movies(radarr_movies, trakt_movies): + new_movies_list = [] + + if not radarr_movies or not trakt_movies: + log.error("Inappropriate parameters were supplied") + return None + + try: + # turn radarr movies result into a dict with tmdb id as keys + processed_movies = movies_to_tmdb_dict(radarr_movies) + if not processed_movies: + return None + + # loop list adding to movies that do not already exist + for tmp in trakt_movies: + if 'movie' not in tmp or 'ids' not in tmp['movie'] or 'tmdb' not in tmp['movie']['ids']: + log.debug("Skipping movie because it did not have required fields: %s", tmp) + continue + # check if movie exists in processed_movies + if tmp['movie']['ids']['tmdb'] in processed_movies: + log.debug("Removing existing movie: %s", tmp['movie']['title']) + continue + + new_movies_list.append(tmp) + + 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: ") + return None diff --git a/helpers/sonarr.py b/helpers/sonarr.py new file mode 100644 index 0000000..52c2f91 --- /dev/null +++ b/helpers/sonarr.py @@ -0,0 +1,81 @@ +from misc.log import logger + +log = logger.get_logger(__name__) + + +def series_tag_id_from_network(profile_tags, network_tags, network): + try: + tags = [] + 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) + tags.append(profile_tags[tag_name.lower()]) + if tags: + return tags + except Exception: + log.exception("Exception determining tag to use for network %s: ", network) + return None + + +def readable_tag_from_ids(profile_tag_ids, chosen_tag_ids): + try: + if not chosen_tag_ids: + return None + + tags = [] + for tag_name, tag_id in profile_tag_ids.items(): + if tag_id in chosen_tag_ids: + tags.append(tag_name) + if tags: + return tags + except Exception: + log.exception("Exception building readable tag name list from ids %s: ", chosen_tag_ids) + return None + + +def series_to_tvdb_dict(sonarr_series): + series = {} + try: + for tmp in sonarr_series: + if 'tvdbId' not in tmp: + log.debug("Could not handle show: %s", tmp['title']) + continue + series[tmp['tvdbId']] = tmp + return series + except Exception: + log.exception("Exception processing Sonarr shows to TVDB dict: ") + return None + + +def remove_existing_series(sonarr_series, trakt_series): + new_series_list = [] + + if not sonarr_series or not trakt_series: + log.error("Inappropriate parameters were supplied") + return None + + try: + # turn sonarr series result into a dict with tvdb id as keys + processed_series = series_to_tvdb_dict(sonarr_series) + if not processed_series: + return None + + # loop list adding to series that do not already exist + for tmp in trakt_series: + if 'show' not in tmp or 'ids' not in tmp['show'] or 'tvdb' not in tmp['show']['ids']: + log.debug("Skipping show because it did not have required fields: %s", tmp) + continue + # check if show exists in processed_series + if tmp['show']['ids']['tvdb'] in processed_series: + log.debug("Removing existing show: %s", tmp['show']['title']) + continue + + new_series_list.append(tmp) + + 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 shows from Trakt list: ") + return None diff --git a/misc/str.py b/helpers/str.py similarity index 100% rename from misc/str.py rename to helpers/str.py diff --git a/helpers/trakt.py b/helpers/trakt.py new file mode 100644 index 0000000..be6df47 --- /dev/null +++ b/helpers/trakt.py @@ -0,0 +1,249 @@ +from helpers import str as misc_str +from misc.log import logger + +log = logger.get_logger(__name__) + + +def blacklisted_show_genre(show, genres): + blacklisted = False + try: + if not show['show']['genres']: + log.debug("%s was blacklisted because it had no genres", show['show']['title']) + blacklisted = True + else: + for genre in genres: + if genre.lower() in show['show']['genres']: + log.debug("%s was blacklisted because it has genre: %s", show['show']['title'], genre) + blacklisted = True + break + + except Exception: + log.exception("Exception determining if show has a blacklisted genre %s: ", show) + return blacklisted + + +def blacklisted_show_year(show, earliest_year, latest_year): + blacklisted = False + try: + year = misc_str.get_year_from_timestamp(show['show']['first_aired']) + if not year: + log.debug("%s was blacklisted due to having an unknown first_aired date", show['show']['title']) + blacklisted = True + else: + if year < earliest_year or year > latest_year: + log.debug("%s was blacklisted because it first aired in: %d", show['show']['title'], year) + blacklisted = True + except Exception: + log.exception("Exception determining if show is within min_year and max_year range %s:", show) + return blacklisted + + +def blacklisted_show_country(show, allowed_countries): + blacklisted = False + try: + if not show['show']['country']: + log.debug("%s was blacklisted because it had no country", show['show']['title']) + blacklisted = True + else: + if show['show']['country'].lower() not in allowed_countries: + log.debug("%s was blacklisted because it's from country: %s", show['show']['title'], + show['show']['country']) + blacklisted = True + + except Exception: + log.exception("Exception determining if show was from an allowed country %s: ", show) + return blacklisted + + +def blacklisted_show_network(show, networks): + blacklisted = False + try: + if not show['show']['network']: + log.debug("%s was blacklisted because it had no network", show['show']['title']) + blacklisted = True + else: + for network in networks: + if network.lower() in show['show']['network'].lower(): + log.debug("%s was blacklisted because it's from network: %s", show['show']['title'], + show['show']['network']) + blacklisted = True + break + + except Exception: + log.exception("Exception determining if show is from a blacklisted network %s: ", show) + return blacklisted + + +def blacklisted_show_runtime(show, lowest_runtime): + blacklisted = False + try: + if not show['show']['runtime'] or not isinstance(show['show']['runtime'], int): + log.debug("%s was blacklisted because it had no runtime", show['show']['title']) + 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['show']['runtime']) + blacklisted = True + + except Exception: + log.exception("Exception determining if show had sufficient runtime %s: ", show) + return blacklisted + + +def blacklisted_show_id(show, blacklisted_ids): + blacklisted = False + try: + if not show['show']['ids']['tvdb'] or not isinstance(show['show']['ids']['tvdb'], int): + log.debug("%s was blacklisted because it had an invalid tvdb id", show['show']['title']) + blacklisted = True + elif show['show']['ids']['tvdb'] in blacklisted_ids: + log.debug("%s was blacklisted because it had a blacklisted tvdb id of: %d", show['show']['title'], + show['show']['ids']['tvdb']) + blacklisted = True + + except Exception: + log.exception("Exception determining if show had a blacklisted tvdb id %s: ", show) + return blacklisted + + +def is_show_blacklisted(show, blacklist_settings): + blacklisted = False + try: + if blacklisted_show_year(show, blacklist_settings.blacklisted_min_year, + blacklist_settings.blacklisted_max_year): + blacklisted = True + if blacklisted_show_country(show, blacklist_settings.allowed_countries): + blacklisted = True + if blacklisted_show_genre(show, blacklist_settings.blacklisted_genres): + blacklisted = True + if blacklisted_show_network(show, blacklist_settings.blacklisted_networks): + blacklisted = True + if blacklisted_show_runtime(show, blacklist_settings.blacklisted_min_runtime): + blacklisted = True + if blacklisted_show_id(show, blacklist_settings.blacklisted_tvdb_ids): + blacklisted = True + except Exception: + log.exception("Exception determining if show was blacklisted %s: ", show) + return blacklisted + + +def blacklisted_movie_genre(movie, genres): + blacklisted = False + try: + if not movie['movie']['genres']: + log.debug("%s was blacklisted because it had no genres", movie['movie']['title']) + blacklisted = True + else: + for genre in genres: + if genre.lower() in movie['movie']['genres']: + log.debug("%s was blacklisted because it has genre: %s", movie['movie']['title'], genre) + blacklisted = True + break + + except Exception: + log.exception("Exception determining if movie has a blacklisted genre %s: ", movie) + return blacklisted + + +def blacklisted_movie_year(movie, earliest_year, latest_year): + blacklisted = False + try: + year = movie['movie']['year'] + if year is None or not isinstance(year, int): + log.debug("%s was blacklisted due to having an unknown year", movie['movie']['title']) + blacklisted = True + else: + if int(year) < earliest_year or int(year) > latest_year: + log.debug("%s was blacklisted because it's year is: %d", movie['movie']['title'], int(year)) + blacklisted = True + except Exception: + log.exception("Exception determining if movie is within min_year and max_year ranger %s:", movie) + return blacklisted + + +def blacklisted_movie_country(movie, allowed_countries): + blacklisted = False + try: + if not movie['movie']['country']: + log.debug("%s was blacklisted because it had no country", movie['movie']['title']) + blacklisted = True + else: + if movie['movie']['country'].lower() not in allowed_countries: + log.debug("%s was blacklisted because it's from country: %s", movie['movie']['title'], + movie['movie']['country']) + blacklisted = True + + except Exception: + log.exception("Exception determining if movie was from an allowed country %s: ", movie) + return blacklisted + + +def blacklisted_movie_title(movie, blacklisted_keywords): + blacklisted = False + try: + if not movie['movie']['title']: + log.debug("Blacklisted movie because it had no title: %s", movie) + blacklisted = True + else: + for keyword in blacklisted_keywords: + if keyword.lower() in movie['movie']['title'].lower(): + log.debug("%s was blacklisted because it had title keyword: %s", movie['movie']['title'], keyword) + blacklisted = True + break + + except Exception: + log.exception("Exception determining if movie had a blacklisted title %s: ", movie) + return blacklisted + + +def blacklisted_movie_runtime(movie, lowest_runtime): + blacklisted = False + try: + if not movie['movie']['runtime'] or not isinstance(movie['movie']['runtime'], int): + log.debug("%s was blacklisted because it had no runtime", movie['movie']['title']) + blacklisted = True + elif int(movie['movie']['runtime']) < lowest_runtime: + log.debug("%s was blacklisted because it had a runtime of: %d", movie['movie']['title'], + movie['movie']['runtime']) + blacklisted = True + + except Exception: + log.exception("Exception determining if movie had sufficient runtime %s: ", movie) + return blacklisted + + +def blacklisted_movie_id(movie, blacklisted_ids): + blacklisted = False + try: + if not movie['movie']['ids']['tmdb'] or not isinstance(movie['movie']['ids']['tmdb'], int): + log.debug("%s was blacklisted because it had an invalid tmdb id", movie['movie']['title']) + blacklisted = True + elif movie['movie']['ids']['tmdb'] in blacklisted_ids: + log.debug("%s was blacklisted because it had a blacklisted tmdb id of: %d", movie['movie']['title'], + movie['movie']['ids']['tmdb']) + blacklisted = True + + except Exception: + log.exception("Exception determining if show had a blacklisted tmdb id %s: ", movie) + return blacklisted + + +def is_movie_blacklisted(movie, blacklist_settings): + blacklisted = False + try: + if blacklisted_movie_title(movie, blacklist_settings.blacklist_title_keywords): + blacklisted = True + if blacklisted_movie_year(movie, blacklist_settings.blacklisted_min_year, + blacklist_settings.blacklisted_max_year): + blacklisted = True + if blacklisted_movie_country(movie, blacklist_settings.allowed_countries): + blacklisted = True + if blacklisted_movie_genre(movie, blacklist_settings.blacklisted_genres): + blacklisted = True + if blacklisted_movie_runtime(movie, blacklist_settings.blacklisted_min_runtime): + blacklisted = True + if blacklisted_movie_id(movie, blacklist_settings.blacklisted_tmdb_ids): + blacklisted = True + except Exception: + log.exception("Exception determining if movie was blacklisted %s: ", movie) + return blacklisted diff --git a/media/pvr.py b/media/pvr.py index 8221ae7..f51d827 100644 --- a/media/pvr.py +++ b/media/pvr.py @@ -4,9 +4,9 @@ from abc import ABC, abstractmethod import backoff import requests -from misc import helpers -from misc import str as misc_str -from misc.helpers import backoff_handler +from helpers.misc import backoff_handler +from helpers import str as misc_str +from helpers import misc from misc.log import logger log = logger.get_logger(__name__) @@ -120,7 +120,7 @@ class PVR(ABC): response_json = None if 'json' in req.headers['Content-Type'].lower(): - response_json = helpers.get_response_dict(req.json(), identifier_field, identifier) + response_json = misc.get_response_dict(req.json(), identifier_field, identifier) if (req.status_code == 201 or req.status_code == 200) \ and (response_json and identifier_field in response_json) \ diff --git a/media/radarr.py b/media/radarr.py index 5bb61fa..7d52f92 100644 --- a/media/radarr.py +++ b/media/radarr.py @@ -1,7 +1,7 @@ import backoff +from helpers.misc import backoff_handler, dict_merge from media.pvr import PVR -from misc.helpers import backoff_handler, dict_merge from misc.log import logger log = logger.get_logger(__name__) diff --git a/media/sonarr.py b/media/sonarr.py index 6f287f9..637e5ad 100644 --- a/media/sonarr.py +++ b/media/sonarr.py @@ -2,10 +2,10 @@ import os.path import backoff import requests +from helpers.misc import backoff_handler, dict_merge +from helpers import str as misc_str from media.pvr import PVR -from misc import str as misc_str -from misc.helpers import backoff_handler, dict_merge from misc.log import logger log = logger.get_logger(__name__) diff --git a/media/trakt.py b/media/trakt.py index a2c67c5..8ea69ab 100644 --- a/media/trakt.py +++ b/media/trakt.py @@ -3,7 +3,7 @@ import time import backoff import requests -from misc.helpers import backoff_handler +from helpers.misc import backoff_handler from misc.log import logger log = logger.get_logger(__name__) diff --git a/misc/helpers.py b/misc/helpers.py deleted file mode 100644 index e4f654a..0000000 --- a/misc/helpers.py +++ /dev/null @@ -1,430 +0,0 @@ -from misc import str as misc_str -from misc.log import logger - -log = logger.get_logger(__name__) - - -############################################################ -# SONARR -############################################################ - -def sonarr_series_tag_id_from_network(profile_tags, network_tags, network): - try: - tags = [] - 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) - tags.append(profile_tags[tag_name.lower()]) - if tags: - return tags - except Exception: - log.exception("Exception determining tag to use for network %s: ", network) - return None - - -def sonarr_readable_tag_from_ids(profile_tag_ids, chosen_tag_ids): - try: - if not chosen_tag_ids: - return None - - tags = [] - for tag_name, tag_id in profile_tag_ids.items(): - if tag_id in chosen_tag_ids: - tags.append(tag_name) - if tags: - return tags - except Exception: - log.exception("Exception building readable tag name list from ids %s: ", chosen_tag_ids) - 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 show: %s", tmp['title']) - continue - series[tmp['tvdbId']] = tmp - return series - except Exception: - log.exception("Exception processing Sonarr shows to TVDB dict: ") - return None - - -def sonarr_remove_existing_series(sonarr_series, trakt_series): - new_series_list = [] - - if not sonarr_series or not trakt_series: - log.error("Inappropriate parameters were supplied") - return None - - try: - # turn sonarr series result into a dict with tvdb id as keys - processed_series = sonarr_series_to_tvdb_dict(sonarr_series) - if not processed_series: - return None - - # loop list adding to series that do not already exist - for tmp in trakt_series: - if 'show' not in tmp or 'ids' not in tmp['show'] or 'tvdb' not in tmp['show']['ids']: - log.debug("Skipping show because it did not have required fields: %s", tmp) - continue - # check if show exists in processed_series - if tmp['show']['ids']['tvdb'] in processed_series: - log.debug("Removing existing show: %s", tmp['show']['title']) - continue - - new_series_list.append(tmp) - - 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 shows from Trakt list: ") - return None - - -def trakt_blacklisted_show_genre(show, genres): - blacklisted = False - try: - if not show['show']['genres']: - log.debug("%s was blacklisted because it had no genres", show['show']['title']) - blacklisted = True - else: - for genre in genres: - if genre.lower() in show['show']['genres']: - log.debug("%s was blacklisted because it has genre: %s", show['show']['title'], genre) - blacklisted = True - break - - except Exception: - log.exception("Exception determining if show has a blacklisted genre %s: ", show) - return blacklisted - - -def trakt_blacklisted_show_year(show, earliest_year, latest_year): - blacklisted = False - try: - year = misc_str.get_year_from_timestamp(show['show']['first_aired']) - if not year: - log.debug("%s was blacklisted due to having an unknown first_aired date", show['show']['title']) - blacklisted = True - else: - if year < earliest_year or year > latest_year: - log.debug("%s was blacklisted because it first aired in: %d", show['show']['title'], year) - blacklisted = True - except Exception: - log.exception("Exception determining if show is within min_year and max_year range %s:", show) - return blacklisted - - -def trakt_blacklisted_show_country(show, allowed_countries): - blacklisted = False - try: - if not show['show']['country']: - log.debug("%s was blacklisted because it had no country", show['show']['title']) - blacklisted = True - else: - if show['show']['country'].lower() not in allowed_countries: - log.debug("%s was blacklisted because it's from country: %s", show['show']['title'], - show['show']['country']) - blacklisted = True - - except Exception: - log.exception("Exception determining if show was from an allowed country %s: ", show) - return blacklisted - - -def trakt_blacklisted_show_network(show, networks): - blacklisted = False - try: - if not show['show']['network']: - log.debug("%s was blacklisted because it had no network", show['show']['title']) - blacklisted = True - else: - for network in networks: - if network.lower() in show['show']['network'].lower(): - log.debug("%s was blacklisted because it's from network: %s", show['show']['title'], - show['show']['network']) - blacklisted = True - break - - except Exception: - log.exception("Exception determining if show is from a blacklisted network %s: ", show) - return blacklisted - - -def trakt_blacklisted_show_runtime(show, lowest_runtime): - blacklisted = False - try: - if not show['show']['runtime'] or not isinstance(show['show']['runtime'], int): - log.debug("%s was blacklisted because it had no runtime", show['show']['title']) - 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['show']['runtime']) - blacklisted = True - - except Exception: - log.exception("Exception determining if show had sufficient runtime %s: ", show) - return blacklisted - - -def trakt_blacklisted_show_id(show, blacklisted_ids): - blacklisted = False - try: - if not show['show']['ids']['tvdb'] or not isinstance(show['show']['ids']['tvdb'], int): - log.debug("%s was blacklisted because it had an invalid tvdb id", show['show']['title']) - blacklisted = True - elif show['show']['ids']['tvdb'] in blacklisted_ids: - log.debug("%s was blacklisted because it had a blacklisted tvdb id of: %d", show['show']['title'], - show['show']['ids']['tvdb']) - blacklisted = True - - except Exception: - log.exception("Exception determining if show had a blacklisted tvdb id %s: ", show) - return blacklisted - - -def trakt_is_show_blacklisted(show, blacklist_settings): - blacklisted = False - try: - if trakt_blacklisted_show_year(show, blacklist_settings.blacklisted_min_year, - blacklist_settings.blacklisted_max_year): - blacklisted = True - if trakt_blacklisted_show_country(show, blacklist_settings.allowed_countries): - blacklisted = True - if trakt_blacklisted_show_genre(show, blacklist_settings.blacklisted_genres): - blacklisted = True - if trakt_blacklisted_show_network(show, blacklist_settings.blacklisted_networks): - blacklisted = True - if trakt_blacklisted_show_runtime(show, blacklist_settings.blacklisted_min_runtime): - blacklisted = True - if trakt_blacklisted_show_id(show, blacklist_settings.blacklisted_tvdb_ids): - blacklisted = True - except Exception: - log.exception("Exception determining if show was blacklisted %s: ", show) - return blacklisted - - -############################################################ -# RADARR -############################################################ - -def radarr_movies_to_tmdb_dict(radarr_movies): - movies = {} - try: - for tmp in radarr_movies: - if 'tmdbId' not in tmp: - log.debug("Could not handle movie: %s", tmp['title']) - continue - movies[tmp['tmdbId']] = tmp - return movies - except Exception: - log.exception("Exception processing Radarr movies to TMDB dict: ") - return None - - -def radarr_remove_existing_movies(radarr_movies, trakt_movies): - new_movies_list = [] - - if not radarr_movies or not trakt_movies: - log.error("Inappropriate parameters were supplied") - return None - - try: - # turn radarr movies result into a dict with tmdb id as keys - processed_movies = radarr_movies_to_tmdb_dict(radarr_movies) - if not processed_movies: - return None - - # loop list adding to movies that do not already exist - for tmp in trakt_movies: - if 'movie' not in tmp or 'ids' not in tmp['movie'] or 'tmdb' not in tmp['movie']['ids']: - log.debug("Skipping movie because it did not have required fields: %s", tmp) - continue - # check if movie exists in processed_movies - if tmp['movie']['ids']['tmdb'] in processed_movies: - log.debug("Removing existing movie: %s", tmp['movie']['title']) - continue - - new_movies_list.append(tmp) - - 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: ") - return None - - -def trakt_blacklisted_movie_genre(movie, genres): - blacklisted = False - try: - if not movie['movie']['genres']: - log.debug("%s was blacklisted because it had no genres", movie['movie']['title']) - blacklisted = True - else: - for genre in genres: - if genre.lower() in movie['movie']['genres']: - log.debug("%s was blacklisted because it has genre: %s", movie['movie']['title'], genre) - blacklisted = True - break - - except Exception: - log.exception("Exception determining if movie has a blacklisted genre %s: ", movie) - return blacklisted - - -def trakt_blacklisted_movie_year(movie, earliest_year, latest_year): - blacklisted = False - try: - year = movie['movie']['year'] - if year is None or not isinstance(year, int): - log.debug("%s was blacklisted due to having an unknown year", movie['movie']['title']) - blacklisted = True - else: - if int(year) < earliest_year or int(year) > latest_year: - log.debug("%s was blacklisted because it's year is: %d", movie['movie']['title'], int(year)) - blacklisted = True - except Exception: - log.exception("Exception determining if movie is within min_year and max_year ranger %s:", movie) - return blacklisted - - -def trakt_blacklisted_movie_country(movie, allowed_countries): - blacklisted = False - try: - if not movie['movie']['country']: - log.debug("%s was blacklisted because it had no country", movie['movie']['title']) - blacklisted = True - else: - if movie['movie']['country'].lower() not in allowed_countries: - log.debug("%s was blacklisted because it's from country: %s", movie['movie']['title'], - movie['movie']['country']) - blacklisted = True - - except Exception: - log.exception("Exception determining if movie was from an allowed country %s: ", movie) - return blacklisted - - -def trakt_blacklisted_movie_title(movie, blacklisted_keywords): - blacklisted = False - try: - if not movie['movie']['title']: - log.debug("Blacklisted movie because it had no title: %s", movie) - blacklisted = True - else: - for keyword in blacklisted_keywords: - if keyword.lower() in movie['movie']['title'].lower(): - log.debug("%s was blacklisted because it had title keyword: %s", movie['movie']['title'], keyword) - blacklisted = True - break - - except Exception: - log.exception("Exception determining if movie had a blacklisted title %s: ", movie) - return blacklisted - - -def trakt_blacklisted_movie_runtime(movie, lowest_runtime): - blacklisted = False - try: - if not movie['movie']['runtime'] or not isinstance(movie['movie']['runtime'], int): - log.debug("%s was blacklisted because it had no runtime", movie['movie']['title']) - blacklisted = True - elif int(movie['movie']['runtime']) < lowest_runtime: - log.debug("%s was blacklisted because it had a runtime of: %d", movie['movie']['title'], - movie['movie']['runtime']) - blacklisted = True - - except Exception: - log.exception("Exception determining if movie had sufficient runtime %s: ", movie) - return blacklisted - - -def trakt_blacklisted_movie_id(movie, blacklisted_ids): - blacklisted = False - try: - if not movie['movie']['ids']['tmdb'] or not isinstance(movie['movie']['ids']['tmdb'], int): - log.debug("%s was blacklisted because it had an invalid tmdb id", movie['movie']['title']) - blacklisted = True - elif movie['movie']['ids']['tmdb'] in blacklisted_ids: - log.debug("%s was blacklisted because it had a blacklisted tmdb id of: %d", movie['movie']['title'], - movie['movie']['ids']['tmdb']) - blacklisted = True - - except Exception: - log.exception("Exception determining if show had a blacklisted tmdb id %s: ", movie) - return blacklisted - - -def trakt_is_movie_blacklisted(movie, blacklist_settings): - blacklisted = False - try: - if trakt_blacklisted_movie_title(movie, blacklist_settings.blacklist_title_keywords): - blacklisted = True - if trakt_blacklisted_movie_year(movie, blacklist_settings.blacklisted_min_year, - blacklist_settings.blacklisted_max_year): - blacklisted = True - if trakt_blacklisted_movie_country(movie, blacklist_settings.allowed_countries): - blacklisted = True - if trakt_blacklisted_movie_genre(movie, blacklist_settings.blacklisted_genres): - blacklisted = True - if trakt_blacklisted_movie_runtime(movie, blacklist_settings.blacklisted_min_runtime): - blacklisted = True - if trakt_blacklisted_movie_id(movie, blacklist_settings.blacklisted_tmdb_ids): - blacklisted = True - except Exception: - log.exception("Exception determining if movie was blacklisted %s: ", movie) - return blacklisted - - -############################################################ -# MISC -############################################################ - - -def get_response_dict(response, key_field=None, key_value=None): - found_response = None - try: - if isinstance(response, list): - if not key_field or not key_value: - found_response = response[0] - else: - for result in response: - if isinstance(result, dict) and key_field in result and result[key_field] == key_value: - found_response = result - break - - if not found_response: - log.error("Unable to find a result with key %s where the value is %s", key_field, key_value) - - elif isinstance(response, dict): - found_response = response - else: - log.error("Unexpected response instance type of %s for %s", type(response).__name__, response) - - except Exception: - log.exception("Exception determining response for %s: ", response) - return found_response - - -def backoff_handler(details): - log.warning("Backing off {wait:0.1f} seconds afters {tries} tries " - "calling function {target} with args {args} and kwargs " - "{kwargs}".format(**details)) - - -def dict_merge(dct, merge_dct): - for k, v in merge_dct.items(): - import collections - - if k in dct and isinstance(dct[k], dict) and isinstance(merge_dct[k], collections.Mapping): - dict_merge(dct[k], merge_dct[k]) - else: - dct[k] = merge_dct[k] - - return dct diff --git a/traktarr.py b/traktarr.py index 21af418..f7f5378 100755 --- a/traktarr.py +++ b/traktarr.py @@ -80,7 +80,7 @@ def trakt_authentication(): def show(show_id, folder=None, no_search=False): from media.sonarr import Sonarr from media.trakt import Trakt - from misc import helpers + from helpers import sonarr as sonarr_helper # replace sonarr root_folder if folder is supplied if folder: @@ -128,17 +128,17 @@ def show(show_id, folder=None, no_search=False): trakt_show['year']) # determine which tags to use when adding this series - use_tags = helpers.sonarr_series_tag_id_from_network(profile_tags, cfg.sonarr.tags, + use_tags = sonarr_helper.series_tag_id_from_network(profile_tags, cfg.sonarr.tags, trakt_show['network']) # add show to sonarr if sonarr.add_series(trakt_show['ids']['tvdb'], trakt_show['title'], trakt_show['ids']['slug'], profile_id, cfg.sonarr.root_folder, use_tags, not no_search): log.info("ADDED %s (%d) with tags: %s", trakt_show['title'], trakt_show['year'], - helpers.sonarr_readable_tag_from_ids(profile_tags, use_tags)) + sonarr_helper.readable_tag_from_ids(profile_tags, use_tags)) else: log.error("FAILED adding %s (%d) with tags: %s", trakt_show['title'], trakt_show['year'], - helpers.sonarr_readable_tag_from_ids(profile_tags, use_tags)) + sonarr_helper.readable_tag_from_ids(profile_tags, use_tags)) return @@ -159,7 +159,8 @@ def shows(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_sea authenticate_user=None): from media.sonarr import Sonarr from media.trakt import Trakt - from misc import helpers + from helpers import sonarr as sonarr_helper + from helpers import trakt as trakt_helper added_shows = 0 @@ -250,7 +251,7 @@ def shows(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_sea 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) + processed_series_list = sonarr_helper.remove_existing_series(sonarr_series_list, trakt_series_list) if processed_series_list is None: log.error("Aborting due to failure to remove existing Sonarr shows from retrieved Trakt shows list") if notifications: @@ -276,26 +277,26 @@ def shows(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_sea continue # check if series passes out blacklist criteria inspection - if not helpers.trakt_is_show_blacklisted(series, cfg.filters.shows): + if not trakt_helper.is_show_blacklisted(series, cfg.filters.shows): log.info("Adding: %s | Genres: %s | Network: %s | Country: %s", series['show']['title'], ', '.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, + use_tags = sonarr_helper.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, use_tags, not no_search): log.info("ADDED %s (%d) with tags: %s", series['show']['title'], series['show']['year'], - helpers.sonarr_readable_tag_from_ids(profile_tags, use_tags)) + sonarr_helper.readable_tag_from_ids(profile_tags, use_tags)) if notifications: callback_notify({'event': 'add_show', 'list_type': list_type, 'show': series['show']}) added_shows += 1 else: log.error("FAILED adding %s (%d) with tags: %s", series['show']['title'], series['show']['year'], - helpers.sonarr_readable_tag_from_ids(profile_tags, use_tags)) + sonarr_helper.readable_tag_from_ids(profile_tags, use_tags)) # stop adding shows, if added_shows >= add_limit if add_limit and added_shows >= add_limit: @@ -392,7 +393,8 @@ def movies(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_se authenticate_user=None): from media.radarr import Radarr from media.trakt import Trakt - from misc import helpers + from helpers import radarr as radarr_helper + from helpers import trakt as trakt_helper added_movies = 0 @@ -475,7 +477,7 @@ def movies(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_se log.info("Retrieved Trakt %s movies list, movies found: %d", list_type, len(trakt_movies_list)) # build filtered movie list without movies that exist in radarr - processed_movies_list = helpers.radarr_remove_existing_movies(radarr_movie_list, trakt_movies_list) + processed_movies_list = radarr_helper.remove_existing_movies(radarr_movie_list, trakt_movies_list) if processed_movies_list is None: log.error("Aborting due to failure to remove existing Radarr movies from retrieved Trakt movies list") if notifications: @@ -501,7 +503,7 @@ def movies(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_se continue # check if movie passes out blacklist criteria inspection - if not helpers.trakt_is_movie_blacklisted(movie, cfg.filters.movies): + if not trakt_helper.is_movie_blacklisted(movie, cfg.filters.movies): log.info("Adding: %s (%d) | Genres: %s | Country: %s", movie['movie']['title'], movie['movie']['year'], ', '.join(movie['movie']['genres']), movie['movie']['country'].upper()) # add movie to radarr From a31f46bf913b6d25061bf4e1f68068286bc7216f Mon Sep 17 00:00:00 2001 From: Mitchell Klijs Date: Thu, 10 May 2018 14:16:37 +0200 Subject: [PATCH 09/15] Refactor Trakt class --- helpers/trakt.py | 12 + media/trakt.py | 1047 +++++++++++----------------------------------- 2 files changed, 265 insertions(+), 794 deletions(-) diff --git a/helpers/trakt.py b/helpers/trakt.py index be6df47..6e65ccb 100644 --- a/helpers/trakt.py +++ b/helpers/trakt.py @@ -247,3 +247,15 @@ def is_movie_blacklisted(movie, blacklist_settings): except Exception: log.exception("Exception determining if movie was blacklisted %s: ", movie) return blacklisted + + +def extract_list_user_and_key_from_url(list_url): + try: + import re + list_user = re.search('\/users\/([^/]*)', list_url).group(1) + list_key = re.search('\/lists\/([^/]*)', list_url).group(1) + + return list_user, list_key + except: + log.error('The URL "%s" is not in the correct format', list_url) + exit() diff --git a/media/trakt.py b/media/trakt.py index 8ea69ab..c6111be 100644 --- a/media/trakt.py +++ b/media/trakt.py @@ -3,7 +3,8 @@ import time import backoff import requests -from helpers.misc import backoff_handler +from helpers.misc import backoff_handler, dict_merge +from helpers.trakt import extract_list_user_and_key_from_url from misc.log import logger log = logger.get_logger(__name__) @@ -14,27 +15,106 @@ class Trakt: def __init__(self, cfg): self.cfg = cfg - self.client_id = cfg.trakt.client_id - self.client_secret = cfg.trakt.client_secret - self.headers = { - 'Content-Type': 'application/json', - 'trakt-api-version': '2', - 'trakt-api-key': self.client_id - } + + ############################################################ + # Requests + ############################################################ + + def _make_request(self, url, payload={}, authenticate_user=None): + headers, authenticate_user = self._headers(authenticate_user) + + if authenticate_user: + url = url.replace('{authenticate_user}', authenticate_user) + + # make request + req = requests.get(url, headers=headers, params=payload, timeout=30) + log.debug("Request URL: %s", req.url) + log.debug("Request Payload: %s", payload) + log.debug("Request User: %s", authenticate_user) + log.debug("Response Code: %d", req.status_code) + + return req + + @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) + def _make_item_request(self, url, object_name, payload={}): + payload = dict_merge(payload, {'extended': 'full'}) + + try: + req = self._make_request(url, payload) + + if req.status_code == 200: + resp_json = req.json() + return resp_json + elif req.status_code == 401: + log.error("The authentication to Trakt is revoked. Please re-authenticate.") + exit() + else: + log.error("Failed to retrieve %s, request response: %d", object_name, req.status_code) + return None + except Exception: + log.exception("Exception retrieving %s: ", object_name) + return None + + @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) + def _make_items_request(self, url, limit, languages, type_name, object_name, authenticate_user=None, payload={}, + sleep_between=5): + if languages is None: + languages = ['en'] + + payload = dict_merge(payload, {'extended': 'full', 'limit': limit, 'page': 1, 'languages': ','.join(languages)}) + processed = [] + + type_name = type_name.replace('{authenticate_user}', self._user_used_for_authentication(authenticate_user)) + + try: + while True: + req = self._make_request(url, payload, authenticate_user) + + current_page = payload['page'] + total_pages = 0 if 'X-Pagination-Page-Count' not in req.headers else int( + req.headers['X-Pagination-Page-Count']) + + log.debug("Response Page: %d of %d", current_page, total_pages) + + if req.status_code == 200: + resp_json = req.json() + + for item in resp_json: + if item not in processed: + processed.append(item) + + # check if we have fetched the last page, break if so + if total_pages == 0: + log.debug("There were no more pages to retrieve") + break + elif current_page >= total_pages: + log.debug("There are no more pages to retrieve results from") + break + else: + log.info("There are %d pages left to retrieve results from", total_pages - current_page) + payload['page'] += 1 + time.sleep(sleep_between) + elif req.status_code == 401: + log.error("The authentication to Trakt is revoked. Please re-authenticate.") + exit() + else: + log.error("Failed to retrieve %s %s, request response: %d", type_name, object_name, req.status_code) + break + + if len(processed): + log.debug("Found %d %s %s", len(processed), type_name, object_name) + return processed + return None + except Exception: + log.exception("Exception retrieving %s %s: ", type_name, object_name) + return None def validate_client_id(self): try: - # request trending shows to determine if client_id is valid - payload = {'extended': 'full', 'limit': 1000} - - # make request - req = requests.get( - 'https://api.trakt.tv/shows/anticipated', - headers=self.headers, - params=payload, - timeout=30 + # request anticipated shows to validate client_id + req = self._make_request( + url='https://api.trakt.tv/shows/anticipated', ) - log.debug("Request Response: %d", req.status_code) if req.status_code == 200: return True @@ -44,16 +124,19 @@ class Trakt: return False ############################################################ - # OAuth Authentication Initialisation + # OAuth Authentication ############################################################ def __oauth_request_device_code(self): log.info("We're talking to Trakt to get your verification code. Please wait a moment...") - payload = {'client_id': self.client_id} + payload = {'client_id': self.cfg.trakt.client_id} + + print(self._headers_without_authentication()) # Request device code - req = requests.post('https://api.trakt.tv/oauth/device/code', params=payload, headers=self.headers) + req = requests.post('https://api.trakt.tv/oauth/device/code', params=payload, + headers=self._headers_without_authentication()) device_code_response = req.json() # Display needed information to the user @@ -72,7 +155,7 @@ class Trakt: access_token = access_token_response['access_token'] # But first we need to find out what user this token belongs to - temp_headers = self.headers + temp_headers = self._headers_without_authentication() temp_headers['Authorization'] = 'Bearer ' + access_token req = requests.get('https://api.trakt.tv/users/me', headers=temp_headers) @@ -112,11 +195,12 @@ class Trakt: log.debug('Polling Trakt for the %sth time; %s seconds left', tries, polling_expire - round(time.time() - polling_start)) - payload = {'code': device_code, 'client_id': self.client_id, 'client_secret': self.client_secret, - 'grant_type': 'authorization_code'} + payload = {'code': device_code, 'client_id': self.cfg.trakt.client_id, + 'client_secret': self.cfg.trakt.client_secret, 'grant_type': 'authorization_code'} # Poll Trakt for access token - req = requests.post('https://api.trakt.tv/oauth/device/token', params=payload, headers=self.headers) + req = requests.post('https://api.trakt.tv/oauth/device/token', params=payload, + headers=self._headers_without_authentication()) success, status_code = self.__oauth_process_token_request(req) @@ -130,10 +214,11 @@ class Trakt: return False def __oauth_refresh_access_token(self, refresh_token): - payload = {'refresh_token': refresh_token, 'client_id': self.client_id, 'client_secret': self.client_secret, - 'grant_type': 'refresh_token'} + payload = {'refresh_token': refresh_token, 'client_id': self.cfg.trakt.client_id, + 'client_secret': self.cfg.trakt.client_secret, 'grant_type': 'refresh_token'} - req = requests.post('https://api.trakt.tv/oauth/token', params=payload, headers=self.headers) + req = requests.post('https://api.trakt.tv/oauth/token', params=payload, + headers=self._headers_without_authentication()) success, status_code = self.__oauth_process_token_request(req) @@ -151,819 +236,193 @@ class Trakt: log.exception("Exception occurred when authenticating user") return False - def oauth_headers(self, user): - headers = self.headers - - if user is None: - users = self.cfg['trakt'] - - if 'client_id' in users.keys(): - users.pop('client_id') - - if 'client_secret' in users.keys(): - users.pop('client_secret') + def _get_first_authenticated_user(self): + import copy - if len(users) > 0: - user = list(users.keys())[0] + users = copy.copy(self.cfg.trakt) - log.debug('No user provided, so default to the first user in the config (%s)', user) - elif user not in self.cfg['trakt'].keys(): - log.error( - 'The user %s you specified to use for authentication is not authenticated yet. ' - 'Authenticate the user first, before you use it to retrieve lists.', - user) + if 'client_id' in users.keys(): + users.pop('client_id') - exit() + if 'client_secret' in users.keys(): + users.pop('client_secret') - # If there is no default user, try without authentication - if user is None: - log.info('Using no authentication') + if len(users) > 0: + return list(users.keys())[0] - return headers, user + def _user_is_authenticated(self, user): + return user in self.cfg['trakt'].keys() + def _renew_oauth_token_if_expired(self, user): token_information = self.cfg['trakt'][user] + # Check if the acces_token for the user is expired expires_at = token_information['created_at'] + token_information['expires_in'] - if expires_at < round(time.time()): log.info("The access token for the user %s has expired. We're requesting a new one; please wait a moment.", user) if self.__oauth_refresh_access_token(token_information["refresh_token"]): - log.info("The access token for the user %s has been refreshed. Please restart the application.", - user) - - headers['Authorization'] = 'Bearer ' + token_information['access_token'] - - return headers, user - - ############################################################ - # Shows - ############################################################ - - @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) - def get_show(self, show_id): - try: - # generate payload - payload = {'extended': 'full'} - - # make request - req = requests.get( - 'https://api.trakt.tv/shows/%s' % str(show_id), - headers=self.headers, - params=payload, - timeout=30 - ) - log.debug("Request URL: %s", req.url) - log.debug("Request Payload: %s", payload) - log.debug("Response Code: %d", req.status_code) + log.info("The access token for the user %s has been refreshed. Please restart the application.", user) - if req.status_code == 200: - resp_json = req.json() - return resp_json - else: - log.error("Failed to retrieve show, request response: %d", req.status_code) - return None - - except Exception: - log.exception("Exception retrieving show: ") - return None - - @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) - def get_anticipated_shows(self, limit=1000, languages=None): - try: - processed_shows = [] - - if languages is None: - languages = ['en'] - - # generate payload - payload = {'extended': 'full', 'limit': limit, 'page': 1} - if languages: - payload['languages'] = ','.join(languages) - - # make request - while True: - req = requests.get( - 'https://api.trakt.tv/shows/anticipated', - headers=self.headers, - params=payload, - timeout=30 - ) - log.debug("Request URL: %s", req.url) - log.debug("Request Payload: %s", payload) - log.debug("Response Code: %d", req.status_code) - log.debug("Response Page: %d of %d", payload['page'], - 0 if 'X-Pagination-Page-Count' not in req.headers else int( - req.headers['X-Pagination-Page-Count'])) - - if req.status_code == 200: - resp_json = req.json() - - for show in resp_json: - if show not in processed_shows: - processed_shows.append(show) - - # check if we have fetched the last page, break if so - if 'X-Pagination-Page-Count' not in req.headers or not int(req.headers['X-Pagination-Page-Count']): - log.debug("There was no more pages to retrieve") - break - elif payload['page'] >= int(req.headers['X-Pagination-Page-Count']): - log.debug("There are no more pages to retrieve results from") - break - else: - log.info("There are %d pages left to retrieve results from", - int(req.headers['X-Pagination-Page-Count']) - payload['page']) - payload['page'] += 1 - time.sleep(5) - - else: - log.error("Failed to retrieve anticipated shows, request response: %d", req.status_code) - break - - if len(processed_shows): - log.debug("Found %d anticipated shows", len(processed_shows)) - return processed_shows - return None - except Exception: - log.exception("Exception retrieving anticipated shows: ") - return None - - @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) - def get_watchlist_shows(self, authenticate_user=None, limit=1000, languages=None): - try: - processed_shows = [] - - if languages is None: - languages = ['en'] - - # generate payload - payload = {'extended': 'full', 'limit': limit, 'page': 1} - if languages: - payload['languages'] = ','.join(languages) - - # make request - while True: - headers, authenticate_user = self.oauth_headers(authenticate_user) - - req = requests.get('https://api.trakt.tv/users/' + authenticate_user + '/watchlist/movies', - params=payload, - headers=headers, - timeout=30) - log.debug("Request User: %s", authenticate_user) - log.debug("Request URL: %s", req.url) - log.debug("Request Payload: %s", payload) - log.debug("Response Code: %d", req.status_code) - log.debug("Response Page: %d of %d", payload['page'], - 0 if 'X-Pagination-Page-Count' not in req.headers else int( - req.headers['X-Pagination-Page-Count'])) - - if req.status_code == 200: - resp_json = req.json() - - for show in resp_json: - if show not in processed_shows: - processed_shows.append(show) - - # check if we have fetched the last page, break if so - if 'X-Pagination-Page-Count' not in req.headers or not int(req.headers['X-Pagination-Page-Count']): - log.debug("There was no more pages to retrieve") - break - elif payload['page'] >= int(req.headers['X-Pagination-Page-Count']): - log.debug("There are no more pages to retrieve results from") - break - else: - log.info("There are %d pages left to retrieve results from", - int(req.headers['X-Pagination-Page-Count']) - payload['page']) - payload['page'] += 1 - time.sleep(5) - elif req.status_code == 401: - log.error("The authentication to Trakt is revoked. Please re-authenticate.") - - exit() - else: - log.error("Failed to retrieve shows on watchlist from %s, request response: %d", authenticate_user, - req.status_code) - break - - if len(processed_shows): - log.debug("Found %d shows on watchlist from %s", len(processed_shows), authenticate_user) - - return processed_shows - return None - except Exception: - log.exception("Exception retrieving shows on watchlist") - return None - - @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) - def get_user_list_shows(self, list_url, authenticate_user=None, limit=1000, languages=None): - try: - processed_shows = [] - - if languages is None: - languages = ['en'] - - # generate payload - payload = {'extended': 'full', 'limit': limit, 'page': 1} - if languages: - payload['languages'] = ','.join(languages) - - try: - import re - list_user = re.search('\/users\/([^/]*)', list_url).group(1) - list_key = re.search('\/lists\/([^/]*)', list_url).group(1) - except: - log.error('The URL "%s" is not in the correct format', list_url) + def _user_used_for_authentication(self, user=None): + if user is None: + user = self._get_first_authenticated_user() + elif not self._user_is_authenticated(user): + log.error('The user %s you specified to use for authentication is not authenticated yet. ' + + 'Authenticate the user first, before you use it to retrieve lists.', user) - exit() + exit() - log.debug('Fetching %s from %s', list_key, list_user) + return user - # make request - while True: - headers, authenticate_user = self.oauth_headers(authenticate_user) - - req = requests.get('https://api.trakt.tv/users/' + list_user + '/lists/' + list_key + '/items/shows', - params=payload, - headers=headers, - timeout=30) - log.debug("Request User: %s", authenticate_user) - log.debug("Request URL: %s", req.url) - log.debug("Request Payload: %s", payload) - log.debug("Response Code: %d", req.status_code) - log.debug("Response Page: %d of %d", payload['page'], - 0 if 'X-Pagination-Page-Count' not in req.headers else int( - req.headers['X-Pagination-Page-Count'])) + def _headers_without_authentication(self): + return { + 'Content-Type': 'application/json', + 'trakt-api-version': '2', + 'trakt-api-key': self.cfg.trakt.client_id + } - if req.status_code == 200: - resp_json = req.json() + def _headers(self, user=None): + headers = self._headers_without_authentication() - for show in resp_json: - if show not in processed_shows: - processed_shows.append(show) + user = self._user_used_for_authentication(user) - # check if we have fetched the last page, break if so - if 'X-Pagination-Page-Count' not in req.headers or not int(req.headers['X-Pagination-Page-Count']): - log.debug("There was no more pages to retrieve") - break - elif payload['page'] >= int(req.headers['X-Pagination-Page-Count']): - log.debug("There are no more pages to retrieve results from") - break - else: - log.info("There are %d pages left to retrieve results from", - int(req.headers['X-Pagination-Page-Count']) - payload['page']) - payload['page'] += 1 - time.sleep(5) - elif req.status_code == 401: - log.error("The authentication to Trakt is revoked. Please re-authenticate.") + if user is not None: + self._renew_oauth_token_if_expired(user) + headers['Authorization'] = 'Bearer ' + self.cfg['trakt'][user]['access_token'] + else: + log.info('No user') - exit() - else: - log.error("Failed to retrieve shows on %s from %s, request response: %d", list_key, list_user, - req.status_code) - break + return headers, user - if len(processed_shows): - log.debug("Found %d shows on %s from %s", len(processed_shows), list_key, list_user) + ############################################################ + # Shows + ############################################################ - return processed_shows - return None - except Exception: - log.exception("Exception retrieving shows on user list") - return None + def get_show(self, show_id): + return self._make_item_request( + url='https://api.trakt.tv/shows/%s' % str(show_id), + object_name='show', + ) - @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) def get_trending_shows(self, limit=1000, languages=None): - try: - processed_shows = [] - - if languages is None: - languages = ['en'] - - # generate payload - payload = {'extended': 'full', 'limit': limit, 'page': 1} - if languages: - payload['languages'] = ','.join(languages) - - # make request - while True: - req = requests.get( - 'https://api.trakt.tv/shows/trending', - headers=self.headers, - params=payload, - timeout=30 - ) - log.debug("Request URL: %s", req.url) - log.debug("Request Payload: %s", payload) - log.debug("Response Code: %d", req.status_code) - log.debug("Response Page: %d of %d", payload['page'], - 0 if 'X-Pagination-Page-Count' not in req.headers else int( - req.headers['X-Pagination-Page-Count'])) - - if req.status_code == 200: - resp_json = req.json() - - for show in resp_json: - if show not in processed_shows: - processed_shows.append(show) - - # check if we have fetched the last page, break if so - if 'X-Pagination-Page-Count' not in req.headers or not int(req.headers['X-Pagination-Page-Count']): - log.debug("There was no more pages to retrieve") - break - elif payload['page'] >= int(req.headers['X-Pagination-Page-Count']): - log.debug("There are no more pages to retrieve results from") - break - else: - log.info("There are %d pages left to retrieve results from", - int(req.headers['X-Pagination-Page-Count']) - payload['page']) - payload['page'] += 1 - time.sleep(5) - - else: - log.error("Failed to retrieve trending shows, request response: %d", req.status_code) - break - - if len(processed_shows): - log.debug("Found %d trending shows", len(processed_shows)) - return processed_shows - return None - except Exception: - log.exception("Exception retrieving trending shows: ") - return None + return self._make_items_request( + url='https://api.trakt.tv/shows/trending', + limit=limit, + languages=languages, + object_name='shows', + type_name='trending', + ) - @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) def get_popular_shows(self, limit=1000, languages=None): - try: - processed_shows = [] - - if languages is None: - languages = ['en'] - - # generate payload - payload = {'extended': 'full', 'limit': limit, 'page': 1} - if languages: - payload['languages'] = ','.join(languages) + return self._make_items_request( + url='https://api.trakt.tv/shows/popular', + limit=limit, + languages=languages, + object_name='shows', + type_name='popular', + ) - # make request - while True: - req = requests.get( - 'https://api.trakt.tv/shows/popular', - headers=self.headers, - params=payload, - timeout=30 - ) - log.debug("Request URL: %s", req.url) - log.debug("Request Payload: %s", payload) - log.debug("Response Code: %d", req.status_code) - log.debug("Response Page: %d of %d", payload['page'], - 0 if 'X-Pagination-Page-Count' not in req.headers else int( - req.headers['X-Pagination-Page-Count'])) - - if req.status_code == 200: - resp_json = req.json() + def get_anticipated_shows(self, limit=1000, languages=None): + return self._make_items_request( + url='https://api.trakt.tv/shows/anticipated', + limit=limit, + languages=languages, + object_name='shows', + type_name='anticipated', + ) - # process list so it conforms to standard we expect ( e.g. {"show": {.....}} ) - for show in resp_json: - if show not in processed_shows: - processed_shows.append({'show': show}) + def get_watchlist_shows(self, authenticate_user=None, limit=1000, languages=None): + return self._make_items_request( + url='https://api.trakt.tv/users/{authenticate_user}/watchlist/shows', + authenticate_user=authenticate_user, + limit=limit, + languages=languages, + object_name='shows', + type_name='watchlist from {authenticate_user}', + ) - # check if we have fetched the last page, break if so - if 'X-Pagination-Page-Count' not in req.headers or not int(req.headers['X-Pagination-Page-Count']): - log.debug("There was no more pages to retrieve") - break - elif payload['page'] >= int(req.headers['X-Pagination-Page-Count']): - log.debug("There are no more pages to retrieve results from") - break - else: - log.info("There are %d pages left to retrieve results from", - int(req.headers['X-Pagination-Page-Count']) - payload['page']) - payload['page'] += 1 - time.sleep(5) + def get_user_list_shows(self, list_url, authenticate_user=None, limit=1000, languages=None): + list_user, list_key = extract_list_user_and_key_from_url(list_url) - else: - log.error("Failed to retrieve popular shows, request response: %d", req.status_code) - break + log.debug('Fetching %s from %s', list_key, list_user) - if len(processed_shows): - log.debug("Found %d popular shows", len(processed_shows)) - return processed_shows - return None - except Exception: - log.exception("Exception retrieving popular shows: ") - return None + return self._make_items_request( + url='https://api.trakt.tv/users/' + list_user + '/lists/' + list_key + '/items/shows', + authenticate_user=authenticate_user, + limit=limit, + languages=languages, + object_name='shows', + type_name=(list_key + ' from ' + list_user), + ) ############################################################ # Movies ############################################################ - @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) def get_movie(self, movie_id): - try: - # generate payload - payload = {'extended': 'full'} - - # make request - req = requests.get( - 'https://api.trakt.tv/movies/%s' % str(movie_id), - headers=self.headers, - params=payload, - timeout=30 - ) - log.debug("Request URL: %s", req.url) - log.debug("Request Payload: %s", payload) - log.debug("Response Code: %d", req.status_code) - - if req.status_code == 200: - resp_json = req.json() - return resp_json - else: - log.error("Failed to retrieve movie, request response: %d", req.status_code) - return None - - except Exception: - log.exception("Exception retrieving movie: ") - return None - - @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) - def get_anticipated_movies(self, limit=1000, languages=None): - try: - processed_movies = [] - - if languages is None: - languages = ['en'] - - # generate payload - payload = {'extended': 'full', 'limit': limit, 'page': 1} - if languages: - payload['languages'] = ','.join(languages) - - # make request - while True: - req = requests.get( - 'https://api.trakt.tv/movies/anticipated', - headers=self.headers, - params=payload, - timeout=30 - ) - log.debug("Request URL: %s", req.url) - log.debug("Request Payload: %s", payload) - log.debug("Response Code: %d", req.status_code) - log.debug("Response Page: %d of %d", payload['page'], - 0 if 'X-Pagination-Page-Count' not in req.headers else int( - req.headers['X-Pagination-Page-Count'])) - - if req.status_code == 200: - resp_json = req.json() - - for movie in resp_json: - if movie not in processed_movies: - processed_movies.append(movie) + return self._make_item_request( + url='https://api.trakt.tv/movies/%s' % str(movie_id), + object_name='movie', + ) - # check if we have fetched the last page, break if so - if 'X-Pagination-Page-Count' not in req.headers or not int(req.headers['X-Pagination-Page-Count']): - log.debug("There was no more pages to retrieve") - break - elif payload['page'] >= int(req.headers['X-Pagination-Page-Count']): - log.debug("There are no more pages to retrieve results from") - break - else: - log.info("There are %d pages left to retrieve results from", - int(req.headers['X-Pagination-Page-Count']) - payload['page']) - payload['page'] += 1 - time.sleep(5) - - else: - log.error("Failed to retrieve anticipated movies, request response: %d", req.status_code) - break - - if len(processed_movies): - log.debug("Found %d anticipated movies", len(processed_movies)) - return processed_movies - return None - except Exception: - log.exception("Exception retrieving anticipated movies: ") - return None - - @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) def get_trending_movies(self, limit=1000, languages=None): - try: - processed_movies = [] - - if languages is None: - languages = ['en'] - - # generate payload - payload = {'extended': 'full', 'limit': limit, 'page': 1} - if languages: - payload['languages'] = ','.join(languages) - - # make request - while True: - req = requests.get( - 'https://api.trakt.tv/movies/trending', - headers=self.headers, - params=payload, - timeout=30 - ) - log.debug("Request URL: %s", req.url) - log.debug("Request Payload: %s", payload) - log.debug("Response Code: %d", req.status_code) - log.debug("Response Page: %d of %d", payload['page'], - 0 if 'X-Pagination-Page-Count' not in req.headers else int( - req.headers['X-Pagination-Page-Count'])) - - if req.status_code == 200: - resp_json = req.json() - - for movie in resp_json: - if movie not in processed_movies: - processed_movies.append(movie) + return self._make_items_request( + url='https://api.trakt.tv/movies/trending', + limit=limit, + languages=languages, + object_name='movies', + type_name='trending', + ) - # check if we have fetched the last page, break if so - if 'X-Pagination-Page-Count' not in req.headers or not int(req.headers['X-Pagination-Page-Count']): - log.debug("There was no more pages to retrieve") - break - elif payload['page'] >= int(req.headers['X-Pagination-Page-Count']): - log.debug("There are no more pages to retrieve results from") - break - else: - log.info("There are %d pages left to retrieve results from", - int(req.headers['X-Pagination-Page-Count']) - payload['page']) - payload['page'] += 1 - time.sleep(5) - - else: - log.error("Failed to retrieve trending movies, request response: %d", req.status_code) - break - - if len(processed_movies): - log.debug("Found %d trending movies", len(processed_movies)) - return processed_movies - return None - except Exception: - log.exception("Exception retrieving trending movies: ") - return None - - @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) def get_popular_movies(self, limit=1000, languages=None): - try: - processed_movies = [] - - if languages is None: - languages = ['en'] - - # generate payload - payload = {'extended': 'full', 'limit': limit, 'page': 1} - if languages: - payload['languages'] = ','.join(languages) - - # make request - while True: - req = requests.get( - 'https://api.trakt.tv/movies/popular', - headers=self.headers, - params=payload, - timeout=30 - ) - log.debug("Request URL: %s", req.url) - log.debug("Request Payload: %s", payload) - log.debug("Response Code: %d", req.status_code) - log.debug("Response Page: %d of %d", payload['page'], - 0 if 'X-Pagination-Page-Count' not in req.headers else int( - req.headers['X-Pagination-Page-Count'])) - - if req.status_code == 200: - resp_json = req.json() - - # process list so it conforms to standard we expect ( e.g. {"movie": {.....}} ) - for movie in resp_json: - if movie not in processed_movies: - processed_movies.append({'movie': movie}) + return self._make_items_request( + url='https://api.trakt.tv/movies/popular', + limit=limit, + languages=languages, + object_name='movies', + type_name='popular', + ) - # check if we have fetched the last page, break if so - if 'X-Pagination-Page-Count' not in req.headers or not int(req.headers['X-Pagination-Page-Count']): - log.debug("There was no more pages to retrieve") - break - elif payload['page'] >= int(req.headers['X-Pagination-Page-Count']): - log.debug("There are no more pages to retrieve results from") - break - else: - log.info("There are %d pages left to retrieve results from", - int(req.headers['X-Pagination-Page-Count']) - payload['page']) - payload['page'] += 1 - time.sleep(5) - - else: - log.error("Failed to retrieve popular movies, request response: %d", req.status_code) - break - - if len(processed_movies): - log.debug("Found %d popular movies", len(processed_movies)) - return processed_movies - return None - except Exception: - log.exception("Exception retrieving popular movies: ") - return None + def get_anticipated_movies(self, limit=1000, languages=None): + return self._make_items_request( + url='https://api.trakt.tv/movies/anticipated', + limit=limit, + languages=languages, + object_name='movies', + type_name='anticipated', + ) - @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) def get_boxoffice_movies(self, limit=1000, languages=None): - try: - processed_movies = [] - - if languages is None: - languages = ['en'] - - # generate payload - payload = {'extended': 'full', 'limit': limit, 'page': 1} - if languages: - payload['languages'] = ','.join(languages) - - # make request - while True: - req = requests.get( - 'https://api.trakt.tv/movies/boxoffice', - headers=self.headers, - params=payload, - timeout=30 - ) - log.debug("Request URL: %s", req.url) - log.debug("Request Payload: %s", payload) - log.debug("Response Code: %d", req.status_code) - log.debug("Response Page: %d of %d", payload['page'], - 0 if 'X-Pagination-Page-Count' not in req.headers else int( - req.headers['X-Pagination-Page-Count'])) - - if req.status_code == 200: - resp_json = req.json() - - for movie in resp_json: - if movie not in processed_movies: - processed_movies.append(movie) + return self._make_items_request( + url='https://api.trakt.tv/movies/boxoffice', + limit=limit, + languages=languages, + object_name='movies', + type_name='anticipated', + ) - # check if we have fetched the last page, break if so - if 'X-Pagination-Page-Count' not in req.headers or not int(req.headers['X-Pagination-Page-Count']): - log.debug("There was no more pages to retrieve") - break - elif payload['page'] >= int(req.headers['X-Pagination-Page-Count']): - log.debug("There are no more pages to retrieve results from") - break - else: - log.info("There are %d pages left to retrieve results from", - int(req.headers['X-Pagination-Page-Count']) - payload['page']) - payload['page'] += 1 - time.sleep(5) - - else: - log.error("Failed to retrieve boxoffice movies, request response: %d", req.status_code) - break - - if len(processed_movies): - log.debug("Found %d boxoffice movies", len(processed_movies)) - return processed_movies - return None - except Exception: - log.exception("Exception retrieving boxoffice movies: ") - return None - - @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) def get_watchlist_movies(self, authenticate_user=None, limit=1000, languages=None): - try: - processed_movies = [] - - if languages is None: - languages = ['en'] - - # generate payload - payload = {'extended': 'full', 'limit': limit, 'page': 1} - if languages: - payload['languages'] = ','.join(languages) - - # make request - while True: - headers, authenticate_user = self.oauth_headers(authenticate_user) - - req = requests.get('https://api.trakt.tv/users/' + authenticate_user + '/watchlist/movies', - params=payload, - headers=headers, - timeout=30) - log.debug("Request User: %s", authenticate_user) - log.debug("Request URL: %s", req.url) - log.debug("Request Payload: %s", payload) - log.debug("Response Code: %d", req.status_code) - log.debug("Response Page: %d of %d", payload['page'], - 0 if 'X-Pagination-Page-Count' not in req.headers else int( - req.headers['X-Pagination-Page-Count'])) - - if req.status_code == 200: - resp_json = req.json() - - for show in resp_json: - if show not in processed_movies: - processed_movies.append(show) + return self._make_items_request( + url='https://api.trakt.tv/users/{authenticate_user}/watchlist/movies', + authenticate_user=authenticate_user, + limit=limit, + languages=languages, + object_name='movies', + type_name='watchlist from {authenticate_user}', + ) - # check if we have fetched the last page, break if so - if 'X-Pagination-Page-Count' not in req.headers or not int(req.headers['X-Pagination-Page-Count']): - log.debug("There was no more pages to retrieve") - break - elif payload['page'] >= int(req.headers['X-Pagination-Page-Count']): - log.debug("There are no more pages to retrieve results from") - break - else: - log.info("There are %d pages left to retrieve results from", - int(req.headers['X-Pagination-Page-Count']) - payload['page']) - payload['page'] += 1 - time.sleep(5) - elif req.status_code == 401: - log.error("The authentication to Trakt is revoked. Please re-authenticate.") - - exit() - else: - log.error("Failed to retrieve movies on watchlist from %s, request response: %d", authenticate_user, - req.status_code) - break - - if len(processed_movies): - log.debug("Found %d movies on watchlist from %s", len(processed_movies), authenticate_user) - - return processed_movies - return None - except Exception: - log.exception("Exception retrieving movies on watchlist") - return None - - @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) def get_user_list_movies(self, list_url, authenticate_user=None, limit=1000, languages=None): - try: - processed_movies = [] - - if languages is None: - languages = ['en'] - - # generate payload - payload = {'extended': 'full', 'limit': limit, 'page': 1} - if languages: - payload['languages'] = ','.join(languages) - - try: - import re - list_user = re.search('\/users\/([^/]*)', list_url).group(1) - list_key = re.search('\/lists\/([^/]*)', list_url).group(1) - except: - log.error('The URL "%s" is not in the correct format', list_url) - - log.debug('Fetching %s from %s', list_key, list_user) - - # make request - while True: - headers, authenticate_user = self.oauth_headers(authenticate_user) - - req = requests.get('https://api.trakt.tv/users/' + list_user + '/lists/' + list_key + '/items/movies', - params=payload, - headers=headers, - timeout=30) - log.debug("Request User: %s", authenticate_user) - log.debug("Request URL: %s", req.url) - log.debug("Request Payload: %s", payload) - log.debug("Response Code: %d", req.status_code) - log.debug("Response Page: %d of %d", payload['page'], - 0 if 'X-Pagination-Page-Count' not in req.headers else int( - req.headers['X-Pagination-Page-Count'])) - - if req.status_code == 200: - resp_json = req.json() - - for show in resp_json: - if show not in processed_movies: - processed_movies.append(show) - - # check if we have fetched the last page, break if so - if 'X-Pagination-Page-Count' not in req.headers or not int(req.headers['X-Pagination-Page-Count']): - log.debug("There was no more pages to retrieve") - break - elif payload['page'] >= int(req.headers['X-Pagination-Page-Count']): - log.debug("There are no more pages to retrieve results from") - break - else: - log.info("There are %d pages left to retrieve results from", - int(req.headers['X-Pagination-Page-Count']) - payload['page']) - payload['page'] += 1 - time.sleep(5) - elif req.status_code == 401: - log.error("The authentication to Trakt is revoked. Please re-authenticate.") - - exit() - else: - log.error("Failed to retrieve movies on %s from %s, request response: %d", list_key, - authenticate_user, - req.status_code) - break - - if len(processed_movies): - log.debug("Found %d movies on %s from %s", len(processed_movies), list_key, authenticate_user) - - return processed_movies - return None - except Exception: - log.exception("Exception retrieving movies on user list") - return None + list_user, list_key = extract_list_user_and_key_from_url(list_url) + + log.debug('Fetching %s from %s', list_key, list_user) + + return self._make_items_request( + url='https://api.trakt.tv/users/' + list_user + '/lists/' + list_key + '/items/movies', + authenticate_user=authenticate_user, + limit=limit, + languages=languages, + object_name='movies', + type_name=(list_key + ' from ' + list_user), + ) From 4b36ed2f98ed7cafb6ba1a4fd9f6bc7938477376 Mon Sep 17 00:00:00 2001 From: Mitchell Klijs Date: Thu, 10 May 2018 14:45:26 +0200 Subject: [PATCH 10/15] Refactor main file a bit --- traktarr.py | 247 +++++++++++++++++++--------------------------------- 1 file changed, 91 insertions(+), 156 deletions(-) diff --git a/traktarr.py b/traktarr.py index f7f5378..d4fcf81 100755 --- a/traktarr.py +++ b/traktarr.py @@ -62,13 +62,61 @@ def trakt_authentication(): from media.trakt import Trakt trakt = Trakt(cfg) - response = trakt.oauth_authentication() - - if response: + if trakt.oauth_authentication(): log.info("Authentication information saved; please restart the application") exit() +def validate_trakt(trakt, notifications): + if not trakt.validate_client_id(): + log.error("Aborting due to failure to validate Trakt API Key") + if notifications: + callback_notify({'event': 'error', 'reason': 'Failure to validate Trakt API Key'}) + exit() + else: + log.info("Validated Trakt API Key") + + +def validate_pvr(pvr, type, notifications): + if not pvr.validate_api_key(): + log.error("Aborting due to failure to validate %s URL / API Key", type) + if notifications: + callback_notify({'event': 'error', 'reason': 'Failure to validate %s URL / API Key' % type}) + return None + else: + log.info("Validated %s URL & API Key", type) + + +def get_profile_id(pvr, profile): + # retrieve profile id for requested profile + profile_id = pvr.get_profile_id(profile) + if not profile_id or not profile_id > 0: + log.error("Aborting due to failure to retrieve Profile ID for: %s", profile) + exit() + log.info("Retrieved Profile ID for %s: %d", profile, profile_id) + return profile_id + + +def get_profile_tags(pvr): + profile_tags = pvr.get_tags() + if profile_tags is None: + log.error("Aborting due to failure to retrieve Tag ID's") + exit() + log.info("Retrieved %d Tag ID's", len(profile_tags)) + return profile_tags + + +def get_objects(pvr, type, notifications): + objects_list = pvr.get_objects() + if not objects_list: + log.error("Aborting due to failure to retrieve %s shows list", type) + if notifications: + callback_notify({'event': 'error', 'reason': 'Failure to retrieve %s shows list' % type}) + exit() + log.info("Retrieved %s shows list, shows found: %d", type, len(objects_list)) + return objects_list + + ############################################################ # SHOWS ############################################################ @@ -86,36 +134,14 @@ def show(show_id, folder=None, no_search=False): if folder: cfg['sonarr']['root_folder'] = folder - # validate trakt api_key trakt = Trakt(cfg) - if not trakt.validate_client_id(): - log.error("Aborting due to failure to validate Trakt API Key") - return None - else: - log.info("Validated Trakt API Key") - - # validate sonarr url & api_key sonarr = Sonarr(cfg.sonarr.url, cfg.sonarr.api_key) - if not sonarr.validate_api_key(): - log.error("Aborting due to failure to validate Sonarr URL / API Key") - return None - else: - log.info("Validated Sonarr URL & API Key") - # retrieve profile id for requested profile - profile_id = sonarr.get_profile_id(cfg.sonarr.profile) - if not profile_id or not profile_id > 0: - log.error("Aborting due to failure to retrieve Profile ID for: %s", cfg.sonarr.profile) - return None - else: - log.info("Retrieved Profile ID for %s: %d", cfg.sonarr.profile, profile_id) + validate_trakt(trakt, False) + validate_pvr(sonarr, 'Sonarr', False) - # retrieve profile tags - profile_tags = sonarr.get_tags() - if profile_tags is None: - log.error("Aborting due to failure to retrieve Tag ID's") - else: - log.info("Retrieved %d Tag ID's", len(profile_tags)) + profile_id = get_profile_id(sonarr, cfg.sonarr.profile) + profile_tags = get_profile_tags(sonarr) # get trakt show trakt_show = trakt.get_show(show_id) @@ -128,8 +154,7 @@ def show(show_id, folder=None, no_search=False): trakt_show['year']) # determine which tags to use when adding this series - use_tags = sonarr_helper.series_tag_id_from_network(profile_tags, cfg.sonarr.tags, - trakt_show['network']) + use_tags = sonarr_helper.series_tag_id_from_network(profile_tags, cfg.sonarr.tags, trakt_show['network']) # add show to sonarr if sonarr.add_series(trakt_show['ids']['tvdb'], trakt_show['title'], trakt_show['ids']['slug'], profile_id, @@ -174,73 +199,29 @@ def shows(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_sea # validate trakt client_id trakt = Trakt(cfg) - if not trakt.validate_client_id(): - log.error("Aborting due to failure to validate Trakt API Key") - if notifications: - callback_notify({'event': 'abort', 'type': 'shows', 'list_type': list_type, - 'reason': 'Failure to validate Trakt API Key'}) - return None - else: - log.info("Validated Trakt API Key") - - # validate sonarr url & api_key sonarr = Sonarr(cfg.sonarr.url, cfg.sonarr.api_key) - if not sonarr.validate_api_key(): - log.error("Aborting due to failure to validate Sonarr URL / API Key") - if notifications: - callback_notify({'event': 'abort', 'type': 'shows', 'list_type': list_type, - 'reason': 'Failure to validate Sonarr URL / API Key'}) - return None - else: - log.info("Validated Sonarr URL & API Key") - # retrieve profile id for requested profile - profile_id = sonarr.get_profile_id(cfg.sonarr.profile) - if not profile_id or not profile_id > 0: - log.error("Aborting due to failure to retrieve Profile ID for: %s", cfg.sonarr.profile) - if notifications: - callback_notify({'event': 'abort', 'type': 'shows', 'list_type': list_type, - 'reason': 'Failure to retrieve Sonarr Profile ID of %s' % cfg.sonarr.profile}) - return None - else: - log.info("Retrieved Profile ID for %s: %d", cfg.sonarr.profile, profile_id) + validate_trakt(trakt, notifications) + validate_pvr(sonarr, 'Sonarr', notifications) - # retrieve profile tags - profile_tags = sonarr.get_tags() - if profile_tags is None: - log.error("Aborting due to failure to retrieve Tag ID's") - if notifications: - callback_notify({'event': 'abort', 'type': 'shows', 'list_type': list_type, - 'reason': "Failure to retrieve Sonarr Tag ID's"}) - return None - else: - log.info("Retrieved %d Tag ID's", len(profile_tags)) + profile_id = get_profile_id(sonarr, cfg.sonarr.profile) + profile_tags = get_profile_tags(sonarr) - # get sonarr series list - sonarr_series_list = sonarr.get_objects() - if not sonarr_series_list: - log.error("Aborting due to failure to retrieve Sonarr shows list") - if notifications: - callback_notify({'event': 'abort', 'type': 'shows', 'list_type': list_type, - 'reason': 'Failure to retrieve Sonarr shows list'}) - return None - else: - log.info("Retrieved Sonarr shows list, shows found: %d", len(sonarr_series_list)) + pvr_objects_list = get_objects(sonarr, 'Sonarr', notifications) # get trakt series list - trakt_series_list = None if list_type.lower() == 'anticipated': - trakt_series_list = trakt.get_anticipated_shows() + trakt_objects_list = trakt.get_anticipated_shows() elif list_type.lower() == 'trending': - trakt_series_list = trakt.get_trending_shows() + trakt_objects_list = trakt.get_trending_shows() elif list_type.lower() == 'popular': - trakt_series_list = trakt.get_popular_shows() + trakt_objects_list = trakt.get_popular_shows() elif list_type.lower() == 'watchlist': - trakt_series_list = trakt.get_watchlist_shows(authenticate_user) + trakt_objects_list = trakt.get_watchlist_shows(authenticate_user) else: - trakt_series_list = trakt.get_user_list_shows(list_type, authenticate_user) + trakt_objects_list = trakt.get_user_list_shows(list_type, authenticate_user) - if not trakt_series_list: + if not trakt_objects_list: log.error("Aborting due to failure to retrieve Trakt %s shows list", list_type) if notifications: callback_notify( @@ -248,10 +229,10 @@ def shows(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_sea 'reason': 'Failure to retrieve Trakt %s shows list' % list_type}) return None else: - log.info("Retrieved Trakt %s shows list, shows found: %d", list_type, len(trakt_series_list)) + log.info("Retrieved Trakt %s shows list, shows found: %d", list_type, len(trakt_objects_list)) # build filtered series list without series that exist in sonarr - processed_series_list = sonarr_helper.remove_existing_series(sonarr_series_list, trakt_series_list) + processed_series_list = sonarr_helper.remove_existing_series(pvr_objects_list, trakt_objects_list) if processed_series_list is None: log.error("Aborting due to failure to remove existing Sonarr shows from retrieved Trakt shows list") if notifications: @@ -284,7 +265,7 @@ def shows(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_sea # determine which tags to use when adding this series use_tags = sonarr_helper.series_tag_id_from_network(profile_tags, cfg.sonarr.tags, - series['show']['network']) + 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, use_tags, @@ -335,26 +316,12 @@ def movie(movie_id, folder=None, no_search=False): # validate trakt api_key trakt = Trakt(cfg) - if not trakt.validate_client_id(): - log.error("Aborting due to failure to validate Trakt API Key") - return None - else: - log.info("Validated Trakt API Key") - - # validate radarr url & api_key radarr = Radarr(cfg.radarr.url, cfg.radarr.api_key) - if not radarr.validate_api_key(): - log.error("Aborting due to failure to validate Radarr URL / API Key") - return None - else: - log.info("Validated Radarr URL & API Key") - # retrieve profile id for requested profile - profile_id = radarr.get_profile_id(cfg.radarr.profile) - if not profile_id or not profile_id > 0: - log.error("Aborting due to failure to retrieve Profile ID for: %s", cfg.radarr.profile) - else: - log.info("Retrieved Profile ID for %s: %d", cfg.radarr.profile, profile_id) + validate_trakt(trakt, False) + validate_pvr(radarr, 'Radarr', False) + + profile_id = get_profile_id(radarr, cfg.sonarr.profile) # get trakt movie trakt_movie = trakt.get_movie(movie_id) @@ -408,65 +375,30 @@ def movies(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_se # validate trakt api_key trakt = Trakt(cfg) - if not trakt.validate_client_id(): - log.error("Aborting due to failure to validate Trakt API Key") - if notifications: - callback_notify({'event': 'abort', 'type': 'movies', 'list_type': list_type, - 'reason': 'Failure to validate Trakt API Key'}) - return None - else: - log.info("Validated Trakt API Key") - - # validate radarr url & api_key radarr = Radarr(cfg.radarr.url, cfg.radarr.api_key) - if not radarr.validate_api_key(): - log.error("Aborting due to failure to validate Radarr URL / API Key") - if notifications: - callback_notify( - {'event': 'abort', 'type': 'movies', 'list_type': list_type, - 'reason': 'Failure to validate Radarr URL / API Key'}) - return None - else: - log.info("Validated Radarr URL & API Key") - # retrieve profile id for requested profile - profile_id = radarr.get_profile_id(cfg.radarr.profile) - if not profile_id or not profile_id > 0: - log.error("Aborting due to failure to retrieve Profile ID for: %s", cfg.radarr.profile) - if notifications: - callback_notify({'event': 'abort', 'type': 'movies', 'list_type': list_type, - 'reason': 'Failure to retrieve Radarr Profile ID of %s' % cfg.radarr.profile}) - return None - else: - log.info("Retrieved Profile ID for %s: %d", cfg.radarr.profile, profile_id) + validate_trakt(trakt, notifications) + validate_pvr(radarr, 'Radarr', notifications) - # get radarr movies list - radarr_movie_list = radarr.get_objects() - if not radarr_movie_list: - log.error("Aborting due to failure to retrieve Radarr movies list") - if notifications: - callback_notify({'event': 'abort', 'type': 'movies', 'list_type': list_type, - 'reason': 'Failure to retrieve Radarr movies list'}) - return None - else: - log.info("Retrieved Radarr movies list, movies found: %d", len(radarr_movie_list)) + profile_id = get_profile_id(radarr, cfg.sonarr.profile) + + pvr_objects_list = get_objects(radarr, 'Radarr', notifications) # get trakt movies list - trakt_movies_list = None if list_type.lower() == 'anticipated': - trakt_movies_list = trakt.get_anticipated_movies() + trakt_objects_list = trakt.get_anticipated_movies() elif list_type.lower() == 'trending': - trakt_movies_list = trakt.get_trending_movies() + trakt_objects_list = trakt.get_trending_movies() elif list_type.lower() == 'popular': - trakt_movies_list = trakt.get_popular_movies() + trakt_objects_list = trakt.get_popular_movies() elif list_type.lower() == 'boxoffice': - trakt_movies_list = trakt.get_boxoffice_movies() + trakt_objects_list = trakt.get_boxoffice_movies() elif list_type.lower() == 'watchlist': - trakt_movies_list = trakt.get_watchlist_movies(authenticate_user) + trakt_objects_list = trakt.get_watchlist_movies(authenticate_user) else: - trakt_movies_list = trakt.get_user_list_movies(list_type, authenticate_user) + trakt_objects_list = trakt.get_user_list_movies(list_type, authenticate_user) - if not trakt_movies_list: + if not trakt_objects_list: log.error("Aborting due to failure to retrieve Trakt %s movies list", list_type) if notifications: callback_notify( @@ -474,10 +406,10 @@ def movies(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_se 'reason': 'Failure to retrieve Trakt %s movies list' % list_type}) return None else: - log.info("Retrieved Trakt %s movies list, movies found: %d", list_type, len(trakt_movies_list)) + log.info("Retrieved Trakt %s movies list, movies found: %d", list_type, len(trakt_objects_list)) # build filtered movie list without movies that exist in radarr - processed_movies_list = radarr_helper.remove_existing_movies(radarr_movie_list, trakt_movies_list) + processed_movies_list = radarr_helper.remove_existing_movies(pvr_objects_list, trakt_objects_list) if processed_movies_list is None: log.error("Aborting due to failure to remove existing Radarr movies from retrieved Trakt movies list") if notifications: @@ -556,6 +488,9 @@ def callback_notify(data): elif data['event'] == 'abort': notify.send(message="Aborted adding Trakt %s %s due to: %s" % (data['list_type'], data['type'], data['reason'])) return + elif data['event'] == 'error': + notify.send(message="Error: %s" % data['reason']) + return else: log.error("Unexpected callback: %s", data) return From d9bd91cf393c4b6dfe2391a2075e9dcd17255f18 Mon Sep 17 00:00:00 2001 From: Mitchell Klijs Date: Thu, 17 May 2018 13:08:04 +0200 Subject: [PATCH 11/15] Updated wrong config keys --- traktarr.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/traktarr.py b/traktarr.py index d4fcf81..23e4b8c 100755 --- a/traktarr.py +++ b/traktarr.py @@ -321,7 +321,7 @@ def movie(movie_id, folder=None, no_search=False): validate_trakt(trakt, False) validate_pvr(radarr, 'Radarr', False) - profile_id = get_profile_id(radarr, cfg.sonarr.profile) + profile_id = get_profile_id(radarr, cfg.radarr.profile) # get trakt movie trakt_movie = trakt.get_movie(movie_id) @@ -380,7 +380,7 @@ def movies(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_se validate_trakt(trakt, notifications) validate_pvr(radarr, 'Radarr', notifications) - profile_id = get_profile_id(radarr, cfg.sonarr.profile) + profile_id = get_profile_id(radarr, cfg.radarr.profile) pvr_objects_list = get_objects(radarr, 'Radarr', notifications) From e39daaaad8c20235ee0437c8b480317ebbfa52b5 Mon Sep 17 00:00:00 2001 From: l3uddz <> Date: Fri, 18 May 2018 19:06:28 +0100 Subject: [PATCH 12/15] send genre tag in trakt list request --- media/trakt.py | 23 ++++++++++++++++------- traktarr.py | 12 ++++++------ 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/media/trakt.py b/media/trakt.py index c6111be..90dbc00 100644 --- a/media/trakt.py +++ b/media/trakt.py @@ -57,11 +57,14 @@ class Trakt: @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) def _make_items_request(self, url, limit, languages, type_name, object_name, authenticate_user=None, payload={}, - sleep_between=5): + sleep_between=5, genres=None): if languages is None: languages = ['en'] payload = dict_merge(payload, {'extended': 'full', 'limit': limit, 'page': 1, 'languages': ','.join(languages)}) + if genres: + payload['genres'] = genres + processed = [] type_name = type_name.replace('{authenticate_user}', self._user_used_for_authentication(authenticate_user)) @@ -306,31 +309,34 @@ class Trakt: object_name='show', ) - def get_trending_shows(self, limit=1000, languages=None): + def get_trending_shows(self, limit=1000, languages=None, genres=None): return self._make_items_request( url='https://api.trakt.tv/shows/trending', limit=limit, languages=languages, object_name='shows', type_name='trending', + genres=genres ) - def get_popular_shows(self, limit=1000, languages=None): + def get_popular_shows(self, limit=1000, languages=None, genres=None): return self._make_items_request( url='https://api.trakt.tv/shows/popular', limit=limit, languages=languages, object_name='shows', type_name='popular', + genres=genres ) - def get_anticipated_shows(self, limit=1000, languages=None): + def get_anticipated_shows(self, limit=1000, languages=None, genres=None): return self._make_items_request( url='https://api.trakt.tv/shows/anticipated', limit=limit, languages=languages, object_name='shows', type_name='anticipated', + genres=genres ) def get_watchlist_shows(self, authenticate_user=None, limit=1000, languages=None): @@ -367,31 +373,34 @@ class Trakt: object_name='movie', ) - def get_trending_movies(self, limit=1000, languages=None): + def get_trending_movies(self, limit=1000, languages=None, genres=None): return self._make_items_request( url='https://api.trakt.tv/movies/trending', limit=limit, languages=languages, object_name='movies', type_name='trending', + genres=genres ) - def get_popular_movies(self, limit=1000, languages=None): + def get_popular_movies(self, limit=1000, languages=None, genres=None): return self._make_items_request( url='https://api.trakt.tv/movies/popular', limit=limit, languages=languages, object_name='movies', type_name='popular', + genres=genres ) - def get_anticipated_movies(self, limit=1000, languages=None): + def get_anticipated_movies(self, limit=1000, languages=None, genres=None): return self._make_items_request( url='https://api.trakt.tv/movies/anticipated', limit=limit, languages=languages, object_name='movies', type_name='anticipated', + genres=genres ) def get_boxoffice_movies(self, limit=1000, languages=None): diff --git a/traktarr.py b/traktarr.py index 23e4b8c..cd06078 100755 --- a/traktarr.py +++ b/traktarr.py @@ -211,11 +211,11 @@ def shows(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_sea # get trakt series list if list_type.lower() == 'anticipated': - trakt_objects_list = trakt.get_anticipated_shows() + trakt_objects_list = trakt.get_anticipated_shows(genres=genre) elif list_type.lower() == 'trending': - trakt_objects_list = trakt.get_trending_shows() + trakt_objects_list = trakt.get_trending_shows(genres=genre) elif list_type.lower() == 'popular': - trakt_objects_list = trakt.get_popular_shows() + trakt_objects_list = trakt.get_popular_shows(genres=genre) elif list_type.lower() == 'watchlist': trakt_objects_list = trakt.get_watchlist_shows(authenticate_user) else: @@ -386,11 +386,11 @@ def movies(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_se # get trakt movies list if list_type.lower() == 'anticipated': - trakt_objects_list = trakt.get_anticipated_movies() + trakt_objects_list = trakt.get_anticipated_movies(genres=genre) elif list_type.lower() == 'trending': - trakt_objects_list = trakt.get_trending_movies() + trakt_objects_list = trakt.get_trending_movies(genres=genre) elif list_type.lower() == 'popular': - trakt_objects_list = trakt.get_popular_movies() + trakt_objects_list = trakt.get_popular_movies(genres=genre) elif list_type.lower() == 'boxoffice': trakt_objects_list = trakt.get_boxoffice_movies() elif list_type.lower() == 'watchlist': From 3f0f9a324ac10bc8d82cef299b9a6e1c19e55156 Mon Sep 17 00:00:00 2001 From: James Date: Sat, 26 May 2018 09:50:47 +0100 Subject: [PATCH 13/15] fix popular lists. issue was they are not returned in their own subdict, e.g. {'movie': { ITEM }} --- media/trakt.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/media/trakt.py b/media/trakt.py index 90dbc00..ca73011 100644 --- a/media/trakt.py +++ b/media/trakt.py @@ -84,7 +84,10 @@ class Trakt: for item in resp_json: if item not in processed: - processed.append(item) + if object_name.rstrip('s') not in item and 'title' in item: + processed.append({object_name.rstrip('s'): item}) + else: + processed.append(item) # check if we have fetched the last page, break if so if total_pages == 0: From 20e444330656c519167a08fe04ab2380e5ef3407 Mon Sep 17 00:00:00 2001 From: James Date: Sat, 26 May 2018 10:39:16 +0100 Subject: [PATCH 14/15] improved genre unblacklist & check when -g is supplied. --- helpers/misc.py | 19 +++++++++++++++++++ traktarr.py | 18 ++++++++++-------- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/helpers/misc.py b/helpers/misc.py index 6e0202e..0c6572b 100644 --- a/helpers/misc.py +++ b/helpers/misc.py @@ -44,3 +44,22 @@ def dict_merge(dct, merge_dct): dct[k] = merge_dct[k] return dct + + +def unblacklist_genres(genre, blacklisted_genres): + genres = genre.split(',') + for allow_genre in genres: + if allow_genre in blacklisted_genres: + blacklisted_genres.remove(allow_genre) + return + + +def allowed_genres(genre, object_type, trakt_object): + allowed_object = False + genres = genre.split(',') + + for item in genres: + if item.lower() in trakt_object[object_type]['genres']: + allowed_object = True + break + return allowed_object diff --git a/traktarr.py b/traktarr.py index cd06078..78524c6 100755 --- a/traktarr.py +++ b/traktarr.py @@ -184,14 +184,15 @@ def shows(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_sea authenticate_user=None): from media.sonarr import Sonarr from media.trakt import Trakt + from helpers import misc as misc_helper from helpers import sonarr as sonarr_helper from helpers import trakt as trakt_helper added_shows = 0 # remove genre from shows blacklisted_genres if supplied - if genre and genre in cfg.filters.shows.blacklisted_genres: - cfg['filters']['shows']['blacklisted_genres'].remove(genre) + if genre: + misc_helper.unblacklist_genres(genre, cfg['filters']['shows']['blacklisted_genres']) # replace sonarr root_folder if folder is supplied if folder: @@ -253,8 +254,8 @@ def shows(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_sea for series in sorted_series_list: try: # check if genre matches genre supplied via argument - if genre and genre.lower() not in series['show']['genres']: - log.debug("Skipping: %s because it was not from %s genre", series['show']['title'], genre.lower()) + if genre and not misc_helper.allowed_genres(genre, 'show', series): + log.debug("Skipping: %s because it was not from %s genre(s)", series['show']['title'], genre.lower()) continue # check if series passes out blacklist criteria inspection @@ -360,14 +361,15 @@ def movies(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_se authenticate_user=None): from media.radarr import Radarr from media.trakt import Trakt + from helpers import misc as misc_helper from helpers import radarr as radarr_helper from helpers import trakt as trakt_helper added_movies = 0 # remove genre from movies blacklisted_genres if supplied - if genre and genre in cfg.filters.movies.blacklisted_genres: - cfg['filters']['movies']['blacklisted_genres'].remove(genre) + if genre: + misc_helper.unblacklist_genres(genre, cfg['filters']['movies']['blacklisted_genres']) # replace radarr root_folder if folder is supplied if folder: @@ -430,8 +432,8 @@ def movies(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_se for movie in sorted_movies_list: try: # check if genre matches genre supplied via argument - if genre and genre.lower() not in movie['movie']['genres']: - log.debug("Skipping: %s because it was not from %s genre", movie['movie']['title'], genre.lower()) + if genre and not misc_helper.allowed_genres(genre, 'movie', movie): + log.debug("Skipping: %s because it was not from %s genre(s)", movie['movie']['title'], genre.lower()) continue # check if movie passes out blacklist criteria inspection From b6ce715a5f7d0fd30974de4d7320408c98fd53c4 Mon Sep 17 00:00:00 2001 From: James Date: Sat, 26 May 2018 14:47:13 +0100 Subject: [PATCH 15/15] added allowed_countries to config --- media/trakt.py | 2 +- misc/config.py | 9 +++------ traktarr.py | 12 ++++++------ 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/media/trakt.py b/media/trakt.py index ca73011..ddc2d7a 100644 --- a/media/trakt.py +++ b/media/trakt.py @@ -58,7 +58,7 @@ class Trakt: @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) def _make_items_request(self, url, limit, languages, type_name, object_name, authenticate_user=None, payload={}, sleep_between=5, genres=None): - if languages is None: + if not languages: languages = ['en'] payload = dict_merge(payload, {'extended': 'full', 'limit': limit, 'page': 1, 'languages': ','.join(languages)}) diff --git a/misc/config.py b/misc/config.py index 1f9dea2..f123139 100644 --- a/misc/config.py +++ b/misc/config.py @@ -60,6 +60,7 @@ class Config(object, metaclass=Singleton): 'blacklisted_genres': [], 'blacklisted_networks': [], 'allowed_countries': [], + 'allowed_languages': [], 'blacklisted_min_runtime': 15, 'blacklisted_min_year': 2000, 'blacklisted_max_year': 2019, @@ -72,7 +73,8 @@ class Config(object, metaclass=Singleton): 'blacklisted_max_year': 2019, 'blacklist_title_keywords': [], 'blacklisted_tmdb_ids': [], - 'allowed_countries': [] + 'allowed_countries': [], + 'allowed_languages': [] } }, 'automatic': { @@ -154,11 +156,6 @@ class Config(object, metaclass=Singleton): sub_upgraded = False merged = settings2.copy() - # print(settings1) - # print(settings2) - # print(overwrite) - # print("_______________") - if isinstance(settings1, dict): for k, v in settings1.items(): # missing k diff --git a/traktarr.py b/traktarr.py index 78524c6..b846e66 100755 --- a/traktarr.py +++ b/traktarr.py @@ -212,11 +212,11 @@ def shows(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_sea # get trakt series list if list_type.lower() == 'anticipated': - trakt_objects_list = trakt.get_anticipated_shows(genres=genre) + trakt_objects_list = trakt.get_anticipated_shows(genres=genre, languages=cfg.filters.shows.allowed_languages) elif list_type.lower() == 'trending': - trakt_objects_list = trakt.get_trending_shows(genres=genre) + trakt_objects_list = trakt.get_trending_shows(genres=genre, languages=cfg.filters.shows.allowed_languages) elif list_type.lower() == 'popular': - trakt_objects_list = trakt.get_popular_shows(genres=genre) + trakt_objects_list = trakt.get_popular_shows(genres=genre, languages=cfg.filters.shows.allowed_languages) elif list_type.lower() == 'watchlist': trakt_objects_list = trakt.get_watchlist_shows(authenticate_user) else: @@ -388,11 +388,11 @@ def movies(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_se # get trakt movies list if list_type.lower() == 'anticipated': - trakt_objects_list = trakt.get_anticipated_movies(genres=genre) + trakt_objects_list = trakt.get_anticipated_movies(genres=genre, languages=cfg.filters.movies.allowed_languages) elif list_type.lower() == 'trending': - trakt_objects_list = trakt.get_trending_movies(genres=genre) + trakt_objects_list = trakt.get_trending_movies(genres=genre, languages=cfg.filters.movies.allowed_languages) elif list_type.lower() == 'popular': - trakt_objects_list = trakt.get_popular_movies(genres=genre) + trakt_objects_list = trakt.get_popular_movies(genres=genre, languages=cfg.filters.movies.allowed_languages) elif list_type.lower() == 'boxoffice': trakt_objects_list = trakt.get_boxoffice_movies() elif list_type.lower() == 'watchlist':