Merge pull request #5 from l3uddz/develop

Develop
pull/54/head
James 7 years ago committed by desimaniac
commit 0fa1976409

@ -98,8 +98,9 @@ class Radarr:
if req.status_code == 201 and req.json()['tmdbId'] == movie_tmdbid: if req.status_code == 201 and req.json()['tmdbId'] == movie_tmdbid:
log.debug("Successfully added %s (%d)", movie_title, movie_tmdbid) log.debug("Successfully added %s (%d)", movie_title, movie_tmdbid)
return True return True
elif req.status_code == 409 and 'message' in req.text: elif 'json' in req.headers['Content-Type'].lower() and 'message' in req.text:
log.error("Failed to add %s (%d), reason: %s", movie_title, movie_tmdbid, req.json()['message']) log.error("Failed to add %s (%d) - status_code: %d, reason: %s", movie_title, movie_tmdbid,
req.status_code, req.json()['message'])
return False return False
else: else:
log.error("Failed to add %s (%d), unexpected response:\n%s", movie_title, movie_tmdbid, req.text) log.error("Failed to add %s (%d), unexpected response:\n%s", movie_title, movie_tmdbid, req.text)

@ -144,8 +144,9 @@ class Sonarr:
if req.status_code == 201 and req.json()['tvdbId'] == series_tvdbid: if req.status_code == 201 and req.json()['tvdbId'] == series_tvdbid:
log.debug("Successfully added %s (%d)", series_title, series_tvdbid) log.debug("Successfully added %s (%d)", series_title, series_tvdbid)
return True return True
elif req.status_code == 401 and 'errorMessage' in req.text: elif 'json' in req.headers['Content-Type'].lower() and 'errorMessage' in req.text:
log.error("Failed to add %s (%d), reason: %s", series_title, series_tvdbid, req.json()['errorMessage']) log.error("Failed to add %s (%d) - status_code: %d, reason: %s", series_title, series_tvdbid,
req.status_code, req.json()['errorMessage'])
return False return False
else: else:
log.error("Failed to add %s (%d), unexpected response:\n%s", series_title, series_tvdbid, req.text) log.error("Failed to add %s (%d), unexpected response:\n%s", series_title, series_tvdbid, req.text)

@ -1,3 +1,5 @@
import time
import backoff import backoff
import requests import requests
@ -84,6 +86,7 @@ class Trakt:
log.info("There are %d pages left to retrieve results from", log.info("There are %d pages left to retrieve results from",
int(req.headers['X-Pagination-Page-Count']) - payload['page']) int(req.headers['X-Pagination-Page-Count']) - payload['page'])
payload['page'] += 1 payload['page'] += 1
time.sleep(5)
else: else:
log.error("Failed to retrieve anticipated shows, request response: %d", req.status_code) log.error("Failed to retrieve anticipated shows, request response: %d", req.status_code)
@ -139,6 +142,7 @@ class Trakt:
log.info("There are %d pages left to retrieve results from", log.info("There are %d pages left to retrieve results from",
int(req.headers['X-Pagination-Page-Count']) - payload['page']) int(req.headers['X-Pagination-Page-Count']) - payload['page'])
payload['page'] += 1 payload['page'] += 1
time.sleep(5)
else: else:
log.error("Failed to retrieve trending shows, request response: %d", req.status_code) log.error("Failed to retrieve trending shows, request response: %d", req.status_code)
@ -195,6 +199,7 @@ class Trakt:
log.info("There are %d pages left to retrieve results from", log.info("There are %d pages left to retrieve results from",
int(req.headers['X-Pagination-Page-Count']) - payload['page']) int(req.headers['X-Pagination-Page-Count']) - payload['page'])
payload['page'] += 1 payload['page'] += 1
time.sleep(5)
else: else:
log.error("Failed to retrieve popular shows, request response: %d", req.status_code) log.error("Failed to retrieve popular shows, request response: %d", req.status_code)
@ -254,6 +259,7 @@ class Trakt:
log.info("There are %d pages left to retrieve results from", log.info("There are %d pages left to retrieve results from",
int(req.headers['X-Pagination-Page-Count']) - payload['page']) int(req.headers['X-Pagination-Page-Count']) - payload['page'])
payload['page'] += 1 payload['page'] += 1
time.sleep(5)
else: else:
log.error("Failed to retrieve anticipated movies, request response: %d", req.status_code) log.error("Failed to retrieve anticipated movies, request response: %d", req.status_code)
@ -309,6 +315,7 @@ class Trakt:
log.info("There are %d pages left to retrieve results from", log.info("There are %d pages left to retrieve results from",
int(req.headers['X-Pagination-Page-Count']) - payload['page']) int(req.headers['X-Pagination-Page-Count']) - payload['page'])
payload['page'] += 1 payload['page'] += 1
time.sleep(5)
else: else:
log.error("Failed to retrieve trending movies, request response: %d", req.status_code) log.error("Failed to retrieve trending movies, request response: %d", req.status_code)
@ -349,7 +356,7 @@ class Trakt:
if req.status_code == 200: if req.status_code == 200:
resp_json = req.json() resp_json = req.json()
# process list so it conforms to standard we expect ( e.g. {"show": {.....}} ) # process list so it conforms to standard we expect ( e.g. {"movie": {.....}} )
for movie in resp_json: for movie in resp_json:
if movie not in processed_movies: if movie not in processed_movies:
processed_movies.append({'movie': movie}) processed_movies.append({'movie': movie})
@ -365,6 +372,7 @@ class Trakt:
log.info("There are %d pages left to retrieve results from", log.info("There are %d pages left to retrieve results from",
int(req.headers['X-Pagination-Page-Count']) - payload['page']) int(req.headers['X-Pagination-Page-Count']) - payload['page'])
payload['page'] += 1 payload['page'] += 1
time.sleep(5)
else: else:
log.error("Failed to retrieve popular movies, request response: %d", req.status_code) log.error("Failed to retrieve popular movies, request response: %d", req.status_code)
@ -377,3 +385,59 @@ class Trakt:
except Exception: except Exception:
log.exception("Exception retrieving popular movies: ") log.exception("Exception retrieving popular movies: ")
return None return None
@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', params=payload, headers=self.headers,
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)
# 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

@ -19,7 +19,7 @@ base_config = {
'root_folder': '/tv/', 'root_folder': '/tv/',
'tags': { 'tags': {
'amzn': ['hbo', 'amc', 'usa network', 'tnt', 'starz', 'the cw', 'fx', 'fox', 'abc', 'nbc', 'cbs', 'tbs', 'amzn': ['hbo', 'amc', 'usa network', 'tnt', 'starz', 'the cw', 'fx', 'fox', 'abc', 'nbc', 'cbs', 'tbs',
'amazon', 'syfy', 'cinemax'] 'amazon', 'syfy', 'cinemax', 'bravo', 'showtime']
} }
}, },
'radarr': { 'radarr': {
@ -38,15 +38,44 @@ base_config = {
'fox sports'], 'fox sports'],
'allowed_countries': ['us', 'gb', 'ca'], 'allowed_countries': ['us', 'gb', 'ca'],
'blacklisted_min_runtime': 15, 'blacklisted_min_runtime': 15,
'blacklisted_min_year': 2000 'blacklisted_min_year': 2000,
'blacklisted_max_year': 2019
}, },
'movies': { 'movies': {
'blacklisted_genres': ['documentary', 'music'], 'blacklisted_genres': ['documentary', 'music'],
'blacklisted_min_runtime': 60, 'blacklisted_min_runtime': 60,
'blacklisted_min_year': 2000, 'blacklisted_min_year': 2000,
'blacklisted_max_year': 2019,
'blacklist_title_keywords': ['untitled', 'barbie'], 'blacklist_title_keywords': ['untitled', 'barbie'],
'allowed_countries': ['us', 'gb', 'ca'] 'allowed_countries': ['us', 'gb', 'ca']
} }
},
'automatic': {
'movies': {
'interval': 24,
'anticipated': 10,
'trending': 2,
'popular': 3,
'boxoffice': 10
},
'shows': {
'interval': 72,
'anticipated': 100,
'trending': 2,
'popular': 1
}
},
'notifications': {
'verbose': True,
'my slack': {
'service': 'slack',
'webhook_url': ''
},
'my pushover': {
'service': 'pushover',
'app_token': '',
'user_token': ''
}
} }
} }
cfg = None cfg = None

@ -85,7 +85,7 @@ def trakt_blacklisted_show_genre(show, genres):
return blacklisted return blacklisted
def trakt_blacklisted_show_year(show, earliest_year): def trakt_blacklisted_show_year(show, earliest_year, latest_year):
blacklisted = False blacklisted = False
try: try:
year = misc_str.get_year_from_timestamp(show['show']['first_aired']) year = misc_str.get_year_from_timestamp(show['show']['first_aired'])
@ -93,11 +93,11 @@ def trakt_blacklisted_show_year(show, earliest_year):
log.debug("%s was blacklisted due to having an unknown first_aired date", show['show']['title']) log.debug("%s was blacklisted due to having an unknown first_aired date", show['show']['title'])
blacklisted = True blacklisted = True
else: else:
if year < earliest_year: if year < earliest_year or year > latest_year:
log.debug("%s was blacklisted because it first aired in: %d", show['show']['title'], year) log.debug("%s was blacklisted because it first aired in: %d", show['show']['title'], year)
blacklisted = True blacklisted = True
except Exception: except Exception:
log.exception("Exception determining if show is before earliest_year %s:", show) log.exception("Exception determining if show is within min_year and max_year range %s:", show)
return blacklisted return blacklisted
@ -156,7 +156,8 @@ def trakt_blacklisted_show_runtime(show, lowest_runtime):
def trakt_is_show_blacklisted(show, blacklist_settings): def trakt_is_show_blacklisted(show, blacklist_settings):
blacklisted = False blacklisted = False
try: try:
if trakt_blacklisted_show_year(show, blacklist_settings.blacklisted_min_year): if trakt_blacklisted_show_year(show, blacklist_settings.blacklisted_min_year,
blacklist_settings.blacklisted_max_year):
blacklisted = True blacklisted = True
if trakt_blacklisted_show_country(show, blacklist_settings.allowed_countries): if trakt_blacklisted_show_country(show, blacklist_settings.allowed_countries):
blacklisted = True blacklisted = True
@ -240,7 +241,7 @@ def trakt_blacklisted_movie_genre(movie, genres):
return blacklisted return blacklisted
def trakt_blacklisted_movie_year(movie, earliest_year): def trakt_blacklisted_movie_year(movie, earliest_year, latest_year):
blacklisted = False blacklisted = False
try: try:
year = movie['movie']['year'] year = movie['movie']['year']
@ -248,11 +249,11 @@ def trakt_blacklisted_movie_year(movie, earliest_year):
log.debug("%s was blacklisted due to having an unknown year", movie['movie']['title']) log.debug("%s was blacklisted due to having an unknown year", movie['movie']['title'])
blacklisted = True blacklisted = True
else: else:
if int(year) < earliest_year: 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)) log.debug("%s was blacklisted because it's year is: %d", movie['movie']['title'], int(year))
blacklisted = True blacklisted = True
except Exception: except Exception:
log.exception("Exception determining if movie is before earliest_year %s:", movie) log.exception("Exception determining if movie is within min_year and max_year ranger %s:", movie)
return blacklisted return blacklisted
@ -312,7 +313,8 @@ def trakt_is_movie_blacklisted(movie, blacklist_settings):
try: try:
if trakt_blacklisted_movie_title(movie, blacklist_settings.blacklist_title_keywords): if trakt_blacklisted_movie_title(movie, blacklist_settings.blacklist_title_keywords):
blacklisted = True blacklisted = True
if trakt_blacklisted_movie_year(movie, blacklist_settings.blacklisted_min_year): if trakt_blacklisted_movie_year(movie, blacklist_settings.blacklisted_min_year,
blacklist_settings.blacklisted_max_year):
blacklisted = True blacklisted = True
if trakt_blacklisted_movie_country(movie, blacklist_settings.allowed_countries): if trakt_blacklisted_movie_country(movie, blacklist_settings.allowed_countries):
blacklisted = True blacklisted = True

@ -17,7 +17,9 @@ class Logger:
self.root_logger.setLevel(log_level) self.root_logger.setLevel(log_level)
# disable bloat loggers # disable bloat loggers
logging.getLogger("requests").setLevel(logging.WARNING)
logging.getLogger('urllib3').setLevel(logging.ERROR) logging.getLogger('urllib3').setLevel(logging.ERROR)
logging.getLogger('schedule').setLevel(logging.ERROR)
# init console_logger # init console_logger
self.console_handler = logging.StreamHandler(sys.stdout) self.console_handler = logging.StreamHandler(sys.stdout)

@ -13,3 +13,16 @@ def get_year_from_timestamp(timestamp):
except Exception: except Exception:
log.exception("Exception parsing year from %s: ", timestamp) log.exception("Exception parsing year from %s: ", timestamp)
return int(year) if str(year).isdigit() else 0 return int(year) if str(year).isdigit() else 0
def is_ascii(string):
try:
string.encode('ascii')
except UnicodeEncodeError:
return False
except UnicodeDecodeError:
return False
except Exception:
log.exception(u"Exception checking if %r was ascii: ", string)
return False
return True

@ -0,0 +1,54 @@
from misc.log import logger
from .pushover import Pushover
from .slack import Slack
log = logger.get_logger(__name__)
SERVICES = {
'pushover': Pushover,
'slack': Slack
}
class Notifications:
def __init__(self):
self.services = []
def load(self, **kwargs):
if 'service' not in kwargs:
log.error("You must specify a service to load with the service parameter")
return False
elif kwargs['service'] not in SERVICES:
log.error("You specified an invalid service to load: %s", kwargs['service'])
return False
try:
chosen_service = SERVICES[kwargs['service']]
del kwargs['service']
# load service
service = chosen_service(**kwargs)
self.services.append(service)
except Exception:
log.exception("Exception while loading service, kwargs=%r: ", kwargs)
def send(self, **kwargs):
try:
# remove service keyword if supplied
if 'service' in kwargs:
# send notification to specified service
chosen_service = kwargs['service'].lower()
del kwargs['service']
else:
chosen_service = None
# send notification(s)
for service in self.services:
if chosen_service and service.NAME.lower() != chosen_service:
continue
elif service.send(**kwargs):
log.debug("Sent notification with %s", service.NAME)
except Exception:
log.exception("Exception sending notification, kwargs=%r: ", kwargs)

@ -0,0 +1,33 @@
import requests
from misc.log import logger
log = logger.get_logger(__name__)
class Pushover:
NAME = "Pushover"
def __init__(self, app_token, user_token):
self.app_token = app_token
self.user_token = user_token
log.debug("Initialized Pushover notification agent")
def send(self, **kwargs):
if not self.app_token or not self.user_token:
log.error("You must specify an app_token and user_token when initializing this class")
return False
# send notification
try:
payload = {
'token': self.app_token,
'user': self.user_token,
'message': kwargs['message']
}
resp = requests.post('https://api.pushover.net/1/messages.json', data=payload, timeout=30)
return True if resp.status_code == 200 else False
except Exception:
log.exception("Error sending notification to %r", self.user_token)
return False

@ -0,0 +1,38 @@
import requests
from misc.log import logger
log = logger.get_logger(__name__)
class Slack:
NAME = "Slack"
def __init__(self, webhook_url, sender_name='traktarr', sender_icon=':movie_camera:', channel=None):
self.webhook_url = webhook_url
self.sender_name = sender_name
self.sender_icon = sender_icon
self.channel = channel
log.debug("Initialized Slack notification agent")
def send(self, **kwargs):
if not self.webhook_url or not self.sender_name or not self.sender_icon:
log.error("You must specify an webhook_url, sender_name and sender_icon when initializing this class")
return False
# send notification
try:
payload = {
'text': kwargs['message'],
'username': self.sender_name,
'icon_emoji': self.sender_icon,
}
if self.channel:
payload['channel'] = self.channel
resp = requests.post(self.webhook_url, json=payload, timeout=30)
return True if resp.status_code == 200 else False
except Exception:
log.exception("Error sending notification to %r", self.webhook_url)
return False

@ -1,4 +1,5 @@
backoff==1.4.3
schedule==0.4.3
attrdict==2.0.0 attrdict==2.0.0
click==6.7 click==6.7
backoff==1.4.3
requests==2.18.4 requests==2.18.4

@ -0,0 +1,17 @@
# /etc/systemd/system/traktarr.service
[Unit]
Description=traktarr
After=network-online.target unionfs.service
[Service]
User=seed
Group=seed
Type=simple
WorkingDirectory=/opt/traktarr/
ExecStart=/usr/bin/python3 /opt/traktarr/traktarr.py run
Restart=always
RestartSec=10
[Install]
WantedBy=default.target

@ -2,6 +2,7 @@
import time import time
import click import click
import schedule
from media.radarr import Radarr from media.radarr import Radarr
from media.sonarr import Sonarr from media.sonarr import Sonarr
@ -9,6 +10,7 @@ from media.trakt import Trakt
from misc import helpers from misc import helpers
from misc.config import cfg from misc.config import cfg
from misc.log import logger from misc.log import logger
from notifications import Notifications
############################################################ ############################################################
# INIT # INIT
@ -17,9 +19,13 @@ from misc.log import logger
# Logging # Logging
log = logger.get_logger('traktarr') log = logger.get_logger('traktarr')
# Notifications
notify = Notifications()
# Click # Click
@click.group(help='Add new shows & movies to Sonarr/Radarr from Trakt lists.') @click.group(help='Add new shows & movies to Sonarr/Radarr from Trakt lists.')
@click.version_option('1.1.0', prog_name='traktarr')
def app(): def app():
pass pass
@ -34,14 +40,18 @@ def app():
@click.option('--add-limit', '-l', default=0, help='Limit number of shows added to Sonarr.', show_default=True) @click.option('--add-limit', '-l', default=0, help='Limit number of shows added to Sonarr.', show_default=True)
@click.option('--add-delay', '-d', default=2.5, help='Seconds between each add request to Sonarr.', show_default=True) @click.option('--add-delay', '-d', default=2.5, help='Seconds between each add request to Sonarr.', show_default=True)
@click.option('--no-search', is_flag=True, help='Disable search when adding shows to Sonarr.') @click.option('--no-search', is_flag=True, help='Disable search when adding shows to Sonarr.')
def shows(list_type, add_limit=0, add_delay=2.5, no_search=False): @click.option('--notifications', is_flag=True, help='Send notifications.')
def shows(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications=False):
added_shows = 0 added_shows = 0
# validate trakt api_key # validate trakt api_key
trakt = Trakt(cfg.trakt.api_key) trakt = Trakt(cfg.trakt.api_key)
if not trakt.validate_api_key(): if not trakt.validate_api_key():
log.error("Aborting due to failure to validate Trakt API Key") log.error("Aborting due to failure to validate Trakt API Key")
return if notifications:
callback_notify({'event': 'abort', 'type': 'shows', 'list_type': list_type,
'reason': 'Failure to validate Trakt API Key'})
return None
else: else:
log.info("Validated Trakt API Key") log.info("Validated Trakt API Key")
@ -49,7 +59,10 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False):
sonarr = Sonarr(cfg.sonarr.url, cfg.sonarr.api_key) sonarr = Sonarr(cfg.sonarr.url, cfg.sonarr.api_key)
if not sonarr.validate_api_key(): if not sonarr.validate_api_key():
log.error("Aborting due to failure to validate Sonarr URL / API Key") log.error("Aborting due to failure to validate Sonarr URL / API Key")
return if notifications:
callback_notify({'event': 'abort', 'type': 'shows', 'list_type': list_type,
'reason': 'Failure to validate Sonarr URL / API Key'})
return None
else: else:
log.info("Validated Sonarr URL & API Key") log.info("Validated Sonarr URL & API Key")
@ -57,7 +70,10 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False):
profile_id = sonarr.get_profile_id(cfg.sonarr.profile) profile_id = sonarr.get_profile_id(cfg.sonarr.profile)
if not profile_id or not profile_id > 0: if not profile_id or not profile_id > 0:
log.error("Aborting due to failure to retrieve Profile ID for: %s", cfg.sonarr.profile) log.error("Aborting due to failure to retrieve Profile ID for: %s", cfg.sonarr.profile)
return 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: else:
log.info("Retrieved Profile ID for %s: %d", cfg.sonarr.profile, profile_id) log.info("Retrieved Profile ID for %s: %d", cfg.sonarr.profile, profile_id)
@ -65,7 +81,10 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False):
profile_tags = sonarr.get_tags() profile_tags = sonarr.get_tags()
if profile_tags is None: if profile_tags is None:
log.error("Aborting due to failure to retrieve Tag ID's") log.error("Aborting due to failure to retrieve Tag ID's")
return if notifications:
callback_notify({'event': 'abort', 'type': 'shows', 'list_type': list_type,
'reason': "Failure to retrieve Sonarr Tag ID's"})
return None
else: else:
log.info("Retrieved %d Tag ID's", len(profile_tags)) log.info("Retrieved %d Tag ID's", len(profile_tags))
@ -73,7 +92,10 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False):
sonarr_series_list = sonarr.get_series() sonarr_series_list = sonarr.get_series()
if not sonarr_series_list: if not sonarr_series_list:
log.error("Aborting due to failure to retrieve Sonarr shows list") log.error("Aborting due to failure to retrieve Sonarr shows list")
return if notifications:
callback_notify({'event': 'abort', 'type': 'shows', 'list_type': list_type,
'reason': 'Failure to retrieve Sonarr shows list'})
return None
else: else:
log.info("Retrieved Sonarr shows list, shows found: %d", len(sonarr_series_list)) log.info("Retrieved Sonarr shows list, shows found: %d", len(sonarr_series_list))
@ -87,18 +109,29 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False):
trakt_series_list = trakt.get_popular_shows() trakt_series_list = trakt.get_popular_shows()
else: else:
log.error("Aborting due to unknown Trakt list type") log.error("Aborting due to unknown Trakt list type")
return if notifications:
callback_notify({'event': 'abort', 'type': 'shows', 'list_type': list_type,
'reason': 'Failure to determine Trakt list type'})
return None
if not trakt_series_list: if not trakt_series_list:
log.error("Aborting due to failure to retrieve Trakt %s shows list", list_type) log.error("Aborting due to failure to retrieve Trakt %s shows list", list_type)
return if notifications:
callback_notify(
{'event': 'abort', 'type': 'shows', 'list_type': list_type,
'reason': 'Failure to retrieve Trakt %s shows list' % list_type})
return None
else: 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_series_list))
# build filtered series list without series that exist in sonarr # 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 = helpers.sonarr_remove_existing_series(sonarr_series_list, trakt_series_list)
if not processed_series_list: if processed_series_list is None:
log.error("Aborting due to failure to remove existing Sonarr shows from retrieved Trakt shows list") log.error("Aborting due to failure to remove existing Sonarr shows from retrieved Trakt shows list")
return if notifications:
callback_notify({'event': 'abort', 'type': 'shows', 'list_type': list_type,
'reason': 'Failure to remove existing Sonarr shows from retrieved Trakt %s shows list' % list_type
})
return None
else: else:
log.info("Removed existing Sonarr shows from Trakt shows list, shows left to process: %d", log.info("Removed existing Sonarr shows from Trakt shows list, shows left to process: %d",
len(processed_series_list)) len(processed_series_list))
@ -125,6 +158,8 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False):
series['show']['ids']['slug'], profile_id, cfg.sonarr.root_folder, use_tags, series['show']['ids']['slug'], profile_id, cfg.sonarr.root_folder, use_tags,
not no_search): not no_search):
log.info("ADDED %s (%d) with tags: %s", series['show']['title'], series['show']['year'], use_tags) log.info("ADDED %s (%d) with tags: %s", series['show']['title'], series['show']['year'], use_tags)
if notifications:
callback_notify({'event': 'add_show', 'list_type': list_type, 'show': series['show']})
added_shows += 1 added_shows += 1
else: else:
log.error("FAILED adding %s (%d) with tags: %s", series['show']['title'], series['show']['year'], log.error("FAILED adding %s (%d) with tags: %s", series['show']['title'], series['show']['year'],
@ -142,21 +177,35 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False):
log.info("Added %d new show(s) to Sonarr", added_shows) log.info("Added %d new show(s) to Sonarr", added_shows)
# send notification
if notifications:
notify.send(message="Added %d shows from Trakt's %s list" % (added_shows, list_type))
return added_shows
############################################################
# MOVIES
############################################################
@app.command(help='Add new movies to Radarr.') @app.command(help='Add new movies to Radarr.')
@click.option('--list-type', '-t', type=click.Choice(['anticipated', 'trending', 'popular']), @click.option('--list-type', '-t', type=click.Choice(['anticipated', 'trending', 'popular', 'boxoffice']),
help='Trakt list to process.', required=True) help='Trakt list to process.', required=True)
@click.option('--add-limit', '-l', default=0, help='Limit number of movies added to Radarr.', show_default=True) @click.option('--add-limit', '-l', default=0, help='Limit number of movies added to Radarr.', show_default=True)
@click.option('--add-delay', '-d', default=2.5, help='Seconds between each add request to Radarr.', show_default=True) @click.option('--add-delay', '-d', default=2.5, help='Seconds between each add request to Radarr.', show_default=True)
@click.option('--no-search', is_flag=True, help='Disable search when adding movies to Radarr.') @click.option('--no-search', is_flag=True, help='Disable search when adding movies to Radarr.')
def movies(list_type, add_limit=0, add_delay=2.5, no_search=False): @click.option('--notifications', is_flag=True, help='Send notifications.')
def movies(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications=False):
added_movies = 0 added_movies = 0
# validate trakt api_key # validate trakt api_key
trakt = Trakt(cfg.trakt.api_key) trakt = Trakt(cfg.trakt.api_key)
if not trakt.validate_api_key(): if not trakt.validate_api_key():
log.error("Aborting due to failure to validate Trakt API Key") log.error("Aborting due to failure to validate Trakt API Key")
return if notifications:
callback_notify({'event': 'abort', 'type': 'movies', 'list_type': list_type,
'reason': 'Failure to validate Trakt API Key'})
return None
else: else:
log.info("Validated Trakt API Key") log.info("Validated Trakt API Key")
@ -164,7 +213,11 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False):
radarr = Radarr(cfg.radarr.url, cfg.radarr.api_key) radarr = Radarr(cfg.radarr.url, cfg.radarr.api_key)
if not radarr.validate_api_key(): if not radarr.validate_api_key():
log.error("Aborting due to failure to validate Radarr URL / API Key") log.error("Aborting due to failure to validate Radarr URL / API Key")
return if notifications:
callback_notify(
{'event': 'abort', 'type': 'movies', 'list_type': list_type,
'reason': 'Failure to validate Radarr URL / API Key'})
return None
else: else:
log.info("Validated Radarr URL & API Key") log.info("Validated Radarr URL & API Key")
@ -172,7 +225,10 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False):
profile_id = radarr.get_profile_id(cfg.radarr.profile) profile_id = radarr.get_profile_id(cfg.radarr.profile)
if not profile_id or not profile_id > 0: if not profile_id or not profile_id > 0:
log.error("Aborting due to failure to retrieve Profile ID for: %s", cfg.radarr.profile) log.error("Aborting due to failure to retrieve Profile ID for: %s", cfg.radarr.profile)
return 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: else:
log.info("Retrieved Profile ID for %s: %d", cfg.radarr.profile, profile_id) log.info("Retrieved Profile ID for %s: %d", cfg.radarr.profile, profile_id)
@ -180,7 +236,10 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False):
radarr_movie_list = radarr.get_movies() radarr_movie_list = radarr.get_movies()
if not radarr_movie_list: if not radarr_movie_list:
log.error("Aborting due to failure to retrieve Radarr movies list") log.error("Aborting due to failure to retrieve Radarr movies list")
return if notifications:
callback_notify({'event': 'abort', 'type': 'movies', 'list_type': list_type,
'reason': 'Failure to retrieve Radarr movies list'})
return None
else: else:
log.info("Retrieved Radarr movies list, movies found: %d", len(radarr_movie_list)) log.info("Retrieved Radarr movies list, movies found: %d", len(radarr_movie_list))
@ -192,20 +251,33 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False):
trakt_movies_list = trakt.get_trending_movies() trakt_movies_list = trakt.get_trending_movies()
elif list_type.lower() == 'popular': elif list_type.lower() == 'popular':
trakt_movies_list = trakt.get_popular_movies() trakt_movies_list = trakt.get_popular_movies()
elif list_type.lower() == 'boxoffice':
trakt_movies_list = trakt.get_boxoffice_movies()
else: else:
log.error("Aborting due to unknown Trakt list type") log.error("Aborting due to unknown Trakt list type")
return if notifications:
callback_notify({'event': 'abort', 'type': 'movies', 'list_type': list_type,
'reason': 'Failure to determine Trakt list type'})
return None
if not trakt_movies_list: if not trakt_movies_list:
log.error("Aborting due to failure to retrieve Trakt %s movies list", list_type) log.error("Aborting due to failure to retrieve Trakt %s movies list", list_type)
return if notifications:
callback_notify(
{'event': 'abort', 'type': 'movies', 'list_type': list_type,
'reason': 'Failure to retrieve Trakt %s movies list' % list_type})
return None
else: 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_movies_list))
# build filtered movie list without movies that exist in radarr # 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 = helpers.radarr_remove_existing_movies(radarr_movie_list, trakt_movies_list)
if not processed_movies_list: if processed_movies_list is None:
log.error("Aborting due to failure to remove existing Radarr movies from retrieved Trakt movies list") log.error("Aborting due to failure to remove existing Radarr movies from retrieved Trakt movies list")
return if notifications:
callback_notify({'event': 'abort', 'type': 'movies', 'list_type': list_type,
'reason': 'Failure to remove existing Radarr movies from retrieved '
'Trakt %s movies list' % list_type})
return None
else: else:
log.info("Removed existing Radarr movies from Trakt movies list, movies left to process: %d", log.info("Removed existing Radarr movies from Trakt movies list, movies left to process: %d",
len(processed_movies_list)) len(processed_movies_list))
@ -226,6 +298,8 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False):
if radarr.add_movie(movie['movie']['ids']['tmdb'], movie['movie']['title'], movie['movie']['year'], if radarr.add_movie(movie['movie']['ids']['tmdb'], movie['movie']['title'], movie['movie']['year'],
movie['movie']['ids']['slug'], profile_id, cfg.radarr.root_folder, not no_search): movie['movie']['ids']['slug'], profile_id, cfg.radarr.root_folder, not no_search):
log.info("ADDED %s (%d)", movie['movie']['title'], movie['movie']['year']) log.info("ADDED %s (%d)", movie['movie']['title'], movie['movie']['year'])
if notifications:
callback_notify({'event': 'add_movie', 'list_type': list_type, 'movie': movie['movie']})
added_movies += 1 added_movies += 1
else: else:
log.error("FAILED adding %s (%d)", movie['movie']['title'], movie['movie']['year']) log.error("FAILED adding %s (%d)", movie['movie']['title'], movie['movie']['year'])
@ -242,10 +316,155 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False):
log.info("Added %d new movie(s) to Radarr", added_movies) log.info("Added %d new movie(s) to Radarr", added_movies)
# send notification
if notifications:
notify.send(message="Added %d movies from Trakt's %s list" % (added_movies, list_type))
return added_movies
############################################################
# AUTOMATIC
############################################################
def callback_notify(data):
log.debug("Received callback data: %s", data)
# handle event
if data['event'] == 'add_movie':
if cfg.notifications.verbose:
notify.send(
message="Added %s movie: %s (%d)" % (data['list_type'], data['movie']['title'], data['movie']['year']))
return
elif data['event'] == 'add_show':
if cfg.notifications.verbose:
notify.send(
message="Added %s show: %s (%d)" % (data['list_type'], data['show']['title'], data['show']['year']))
return
elif data['event'] == 'abort':
notify.send(message="Aborted adding Trakt %s %s due to: %s" % (data['list_type'], data['type'], data['reason']))
return
else:
log.error("Unexpected callback: %s", data)
return
def automatic_shows(add_delay=2.5, no_search=False, notifications=False):
total_shows_added = 0
try:
log.info("Started")
for list_type, type_amount in cfg.automatic.shows.items():
if list_type.lower() == 'interval':
continue
elif type_amount <= 0:
log.info("Skipped Trakt's %s shows list", list_type)
continue
else:
log.info("Adding %d shows from Trakt's %s list", type_amount, list_type)
# run shows
added_shows = shows.callback(list_type=list_type, add_limit=type_amount,
add_delay=add_delay, no_search=no_search,
notifications=notifications)
if added_shows is None:
log.error("Failed adding shows from Trakt's %s list", list_type)
time.sleep(10)
continue
total_shows_added += added_shows
# sleep
time.sleep(10)
log.info("Finished, added %d shows total to Sonarr!", total_shows_added)
# send notification
if notifications:
notify.send(message="Added %d shows total to Sonarr!" % total_shows_added)
except Exception:
log.exception("Exception while automatically adding shows: ")
return
def automatic_movies(add_delay=2.5, no_search=False, notifications=False):
total_movies_added = 0
try:
log.info("Started")
for list_type, type_amount in cfg.automatic.movies.items():
if list_type.lower() == 'interval':
continue
elif type_amount <= 0:
log.info("Skipped Trakt's %s movies list", list_type)
continue
else:
log.info("Adding %d movies from Trakt's %s list", type_amount, list_type)
# run movies
added_movies = movies.callback(list_type=list_type, add_limit=type_amount,
add_delay=add_delay, no_search=no_search,
notifications=notifications)
if added_movies is None:
log.error("Failed adding movies from Trakt's %s list", list_type)
time.sleep(10)
continue
total_movies_added += added_movies
# sleep
time.sleep(10)
log.info("Finished, added %d movies total to Radarr!", total_movies_added)
# send notification
if notifications:
notify.send(message="Added %d movies total to Radarr!" % total_movies_added)
except Exception:
log.exception("Exception while automatically adding movies: ")
return
@app.command(help='Run in automatic mode.')
@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('--no-notifications', is_flag=True, help="Disable notifications.")
def run(add_delay=2.5, no_search=False, no_notifications=False):
# add tasks to repeat
schedule.every(cfg.automatic.movies.interval).hours.do(automatic_movies, add_delay, no_search,
not no_notifications)
schedule.every(cfg.automatic.shows.interval).hours.do(automatic_shows, add_delay, no_search, not no_notifications)
# run schedule
log.info("Automatic mode is now running...")
while True:
try:
schedule.run_pending()
except Exception:
log.exception("Unhandled exception occurred while processing scheduled tasks: ")
else:
time.sleep(1)
############################################################
# MISC
############################################################
def init_notifications():
try:
for notification_name, notification_config in cfg.notifications.items():
if notification_name.lower() == 'verbose':
continue
notify.load(**notification_config)
except Exception:
log.exception("Exception initializing notification agents: ")
return
############################################################ ############################################################
# MAIN # MAIN
############################################################ ############################################################
if __name__ == "__main__": if __name__ == "__main__":
init_notifications()
app() app()

Loading…
Cancel
Save