Merge pull request #25 from mitchellklijs/refactoredCode

Refactored code
pull/54/head
James 7 years ago committed by desimaniac
commit 6124790abd

@ -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

@ -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

@ -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

@ -1,92 +1,10 @@
from misc import str as misc_str
from helpers 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):
def blacklisted_show_genre(show, genres):
blacklisted = False
try:
if not show['show']['genres']:
@ -104,7 +22,7 @@ def trakt_blacklisted_show_genre(show, genres):
return blacklisted
def trakt_blacklisted_show_year(show, earliest_year, latest_year):
def blacklisted_show_year(show, earliest_year, latest_year):
blacklisted = False
try:
year = misc_str.get_year_from_timestamp(show['show']['first_aired'])
@ -120,7 +38,7 @@ def trakt_blacklisted_show_year(show, earliest_year, latest_year):
return blacklisted
def trakt_blacklisted_show_country(show, allowed_countries):
def blacklisted_show_country(show, allowed_countries):
blacklisted = False
try:
if not show['show']['country']:
@ -137,7 +55,7 @@ def trakt_blacklisted_show_country(show, allowed_countries):
return blacklisted
def trakt_blacklisted_show_network(show, networks):
def blacklisted_show_network(show, networks):
blacklisted = False
try:
if not show['show']['network']:
@ -156,7 +74,7 @@ def trakt_blacklisted_show_network(show, networks):
return blacklisted
def trakt_blacklisted_show_runtime(show, lowest_runtime):
def blacklisted_show_runtime(show, lowest_runtime):
blacklisted = False
try:
if not show['show']['runtime'] or not isinstance(show['show']['runtime'], int):
@ -172,7 +90,7 @@ def trakt_blacklisted_show_runtime(show, lowest_runtime):
return blacklisted
def trakt_blacklisted_show_id(show, blacklisted_ids):
def blacklisted_show_id(show, blacklisted_ids):
blacklisted = False
try:
if not show['show']['ids']['tvdb'] or not isinstance(show['show']['ids']['tvdb'], int):
@ -188,79 +106,28 @@ def trakt_blacklisted_show_id(show, blacklisted_ids):
return blacklisted
def trakt_is_show_blacklisted(show, blacklist_settings):
def 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):
if 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):
if blacklisted_show_country(show, blacklist_settings.allowed_countries):
blacklisted = True
if trakt_blacklisted_show_genre(show, blacklist_settings.blacklisted_genres):
if blacklisted_show_genre(show, blacklist_settings.blacklisted_genres):
blacklisted = True
if trakt_blacklisted_show_network(show, blacklist_settings.blacklisted_networks):
if blacklisted_show_network(show, blacklist_settings.blacklisted_networks):
blacklisted = True
if trakt_blacklisted_show_runtime(show, blacklist_settings.blacklisted_min_runtime):
if blacklisted_show_runtime(show, blacklist_settings.blacklisted_min_runtime):
blacklisted = True
if trakt_blacklisted_show_id(show, blacklist_settings.blacklisted_tvdb_ids):
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
############################################################
# 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):
def blacklisted_movie_genre(movie, genres):
blacklisted = False
try:
if not movie['movie']['genres']:
@ -278,7 +145,7 @@ def trakt_blacklisted_movie_genre(movie, genres):
return blacklisted
def trakt_blacklisted_movie_year(movie, earliest_year, latest_year):
def blacklisted_movie_year(movie, earliest_year, latest_year):
blacklisted = False
try:
year = movie['movie']['year']
@ -294,7 +161,7 @@ def trakt_blacklisted_movie_year(movie, earliest_year, latest_year):
return blacklisted
def trakt_blacklisted_movie_country(movie, allowed_countries):
def blacklisted_movie_country(movie, allowed_countries):
blacklisted = False
try:
if not movie['movie']['country']:
@ -311,7 +178,7 @@ def trakt_blacklisted_movie_country(movie, allowed_countries):
return blacklisted
def trakt_blacklisted_movie_title(movie, blacklisted_keywords):
def blacklisted_movie_title(movie, blacklisted_keywords):
blacklisted = False
try:
if not movie['movie']['title']:
@ -329,7 +196,7 @@ def trakt_blacklisted_movie_title(movie, blacklisted_keywords):
return blacklisted
def trakt_blacklisted_movie_runtime(movie, lowest_runtime):
def blacklisted_movie_runtime(movie, lowest_runtime):
blacklisted = False
try:
if not movie['movie']['runtime'] or not isinstance(movie['movie']['runtime'], int):
@ -345,7 +212,7 @@ def trakt_blacklisted_movie_runtime(movie, lowest_runtime):
return blacklisted
def trakt_blacklisted_movie_id(movie, blacklisted_ids):
def blacklisted_movie_id(movie, blacklisted_ids):
blacklisted = False
try:
if not movie['movie']['ids']['tmdb'] or not isinstance(movie['movie']['ids']['tmdb'], int):
@ -361,52 +228,34 @@ def trakt_blacklisted_movie_id(movie, blacklisted_ids):
return blacklisted
def trakt_is_movie_blacklisted(movie, blacklist_settings):
def is_movie_blacklisted(movie, blacklist_settings):
blacklisted = False
try:
if trakt_blacklisted_movie_title(movie, blacklist_settings.blacklist_title_keywords):
if 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):
if 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):
if blacklisted_movie_country(movie, blacklist_settings.allowed_countries):
blacklisted = True
if trakt_blacklisted_movie_genre(movie, blacklist_settings.blacklisted_genres):
if blacklisted_movie_genre(movie, blacklist_settings.blacklisted_genres):
blacklisted = True
if trakt_blacklisted_movie_runtime(movie, blacklist_settings.blacklisted_min_runtime):
if blacklisted_movie_runtime(movie, blacklist_settings.blacklisted_min_runtime):
blacklisted = True
if trakt_blacklisted_movie_id(movie, blacklist_settings.blacklisted_tmdb_ids):
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
############################################################
# MISC
############################################################
def get_response_dict(response, key_field=None, key_value=None):
found_response = None
def extract_list_user_and_key_from_url(list_url):
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
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()

@ -0,0 +1,141 @@
import os.path
from abc import ABC, abstractmethod
import backoff
import requests
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__)
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 = 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) \
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

@ -1,143 +1,28 @@
import os.path
import backoff
import requests
from misc import helpers
from misc import str as misc_str
from helpers.misc import backoff_handler, dict_merge
from media.pvr import PVR
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)

@ -2,115 +2,18 @@ import os.path
import backoff
import requests
from helpers.misc import backoff_handler, dict_merge
from misc import helpers
from misc import str as misc_str
from helpers import str as misc_str
from media.pvr import PVR
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)

File diff suppressed because it is too large Load Diff

@ -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
############################################################
@ -80,42 +128,20 @@ 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:
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,17 +154,16 @@ 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,
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,
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 +184,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
@ -173,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_series()
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(
@ -247,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 = helpers.sonarr_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:
@ -276,26 +258,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,
series['show']['network'])
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:
@ -334,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.radarr.profile)
# get trakt movie
trakt_movie = trakt.get_movie(movie_id)
@ -392,7 +360,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
@ -406,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_movies()
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.radarr.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(
@ -472,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 = helpers.radarr_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:
@ -501,7 +435,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
@ -554,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

Loading…
Cancel
Save