initial commit

pull/1/head
l3uddz 7 years ago
commit e176d51f26

30
.gitignore vendored

@ -0,0 +1,30 @@
# User-specific stuff:
.idea
## File-based project format:
*.iws
# IntelliJ
/out/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
*.pyc
# logs
*.log*
# databases
*.db
# configs
*.cfg
*.json
# generators
*.bat
# Pyenv
**/.python-version

@ -0,0 +1,2 @@
# traktarr
Script to add new series & movies to Sonarr/Radarr based on Trakt lists.

@ -0,0 +1 @@
from media import trakt, sonarr, radarr

@ -0,0 +1,109 @@
from urllib.parse import urljoin
import backoff
import requests
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(urljoin(self.server_url, 'api/system/status'), headers=self.headers, timeout=30)
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(urljoin(self.server_url, 'api/movie'), headers=self.headers, timeout=30)
log.debug("Request URL: %s", req.url)
log.debug("Request Response: %d", req.status_code)
if req.status_code == 200:
resp_json = req.json()
log.debug("Found %d 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(urljoin(self.server_url, 'api/profile'), headers=self.headers, timeout=30)
log.debug("Request URL: %s", req.url)
log.debug("Request Response: %d", req.status_code)
if req.status_code == 200:
resp_json = req.json()
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 add_movie(self, movie_tmdbid, movie_title, movie_year, 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': '',
'addOptions': {'ignoreEpisodesWithFiles': False, 'ignoreEpisodesWithoutFiles': False,
'searchForMovie': search_missing}
}
# make request
req = requests.post(urljoin(self.server_url, 'api/movie'), json=payload, headers=self.headers, timeout=30)
log.debug("Request URL: %s", req.url)
log.debug("Request Payload: %s", payload)
log.debug("Request Response: %d", req.status_code)
if req.status_code == 201 and req.json()['tmdbId'] == movie_tmdbid:
log.debug("Successfully added %s (%d)", movie_title, movie_tmdbid)
return True
elif req.status_code == 401:
log.error("Failed to add %s (%d), reason: %s", movie_title, movie_tmdbid,
req.json()['errorMessage'] if '{' in req.text else "\n{}".format(req.text))
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

@ -0,0 +1,110 @@
from urllib.parse import urljoin
import backoff
import requests
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 = {
'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(urljoin(self.server_url, 'api/system/status'), headers=self.headers, timeout=30)
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(urljoin(self.server_url, 'api/series'), headers=self.headers, timeout=30)
log.debug("Request URL: %s", req.url)
log.debug("Request Response: %d", req.status_code)
if req.status_code == 200:
resp_json = req.json()
log.debug("Found %d series", len(resp_json))
return resp_json
else:
log.error("Failed to retrieve all series, request response: %d", req.status_code)
except Exception:
log.exception("Exception retrieving series: ")
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(urljoin(self.server_url, 'api/profile'), headers=self.headers, timeout=30)
log.debug("Request URL: %s", req.url)
log.debug("Request Response: %d", req.status_code)
if req.status_code == 200:
resp_json = req.json()
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 add_series(self, series_tvdbid, series_title, profile_id, root_folder, search_missing=False):
try:
# generate payload
payload = {
'tvdbId': series_tvdbid, 'title': series_title,
'qualityProfileId': profile_id, 'images': [],
'seasons': [], 'seasonFolder': True,
'monitored': True, 'rootFolderPath': root_folder,
'addOptions': {'ignoreEpisodesWithFiles': False,
'ignoreEpisodesWithoutFiles': False,
'searchForMissingEpisodes': search_missing}
}
# make request
req = requests.post(urljoin(self.server_url, 'api/series'), json=payload, headers=self.headers, timeout=30)
log.debug("Request URL: %s", req.url)
log.debug("Request Payload: %s", payload)
log.debug("Request Response: %d", req.status_code)
if req.status_code == 201 and req.json()['tvdbId'] == series_tvdbid:
log.debug("Successfully added %s (%d)", series_title, series_tvdbid)
return True
elif req.status_code == 401:
log.error("Failed to add %s (%d), reason: %s", series_title, series_tvdbid,
req.json()['errorMessage'] if '{' in req.text else "\n{}".format(req.text))
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 series %s (%d): ", series_title, series_tvdbid)
return None

@ -0,0 +1,379 @@
import backoff
import requests
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:
def __init__(self, api_key):
self.api_key = api_key
self.headers = {
'Content-Type': 'application/json',
'trakt-api-version': '2',
'trakt-api-key': self.api_key
}
def validate_api_key(self):
try:
# request trending shows to determine if api_key is valid
payload = {'extended': 'full', 'limit': 1000}
# make request
req = requests.get('https://api.trakt.tv/shows/anticipated', params=payload, headers=self.headers,
timeout=30)
log.debug("Request Response: %d", req.status_code)
if req.status_code == 200:
return True
return False
except Exception:
log.exception("Exception validating api_key: ")
return False
############################################################
# Shows
############################################################
@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', 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 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
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_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', 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 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
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
@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)
# make request
while True:
req = requests.get('https://api.trakt.tv/shows/popular', 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()
# 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})
# 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
else:
log.error("Failed to retrieve popular shows, request response: %d", req.status_code)
break
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
############################################################
# Movies
############################################################
@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', 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
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', 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
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', 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()
# process list so it conforms to standard we expect ( e.g. {"show": {.....}} )
for movie in resp_json:
if movie not in processed_movies:
processed_movies.append({'movie': 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
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

@ -0,0 +1,2 @@
from misc import config, str, helpers
from misc.log import logger

@ -0,0 +1,101 @@
import json
import os
import sys
from attrdict import AttrDict
config_path = os.path.join(os.path.dirname(sys.argv[0]), 'config.json')
base_config = {
'core': {
'debug': False
},
'trakt': {
'api_key': ''
},
'sonarr': {
'url': 'http://localhost:8989',
'api_key': '',
'profile': 'WEBDL-1080p',
'root_folder': '/tv/'
},
'radarr': {
'url': 'http://localhost:8989',
'api_key': '',
'profile': 'Remux',
'root_folder': '/movies/'
},
'filters': {
'shows': {
'blacklisted_genres': ['animation', 'game-show', 'talk-show', 'home-and-garden', 'children', 'reality',
'anime', 'news', 'documentary', 'special-interest'],
'blacklisted_networks': ['twitch', 'youtube', 'nickelodeon', 'hallmark', 'reelzchannel', 'disney',
'cnn', 'cbbc', 'the movie network', 'teletoon', 'cartoon network', 'espn',
'yahoo!',
'fox sports'],
'allowed_countries': ['us', 'gb', 'ca'],
'blacklisted_min_runtime': 15,
'blacklisted_min_year': 2000
},
'movies': {
'blacklisted_genres': ['documentary', 'music'],
'blacklisted_min_runtime': 60,
'blacklisted_min_year': 2000,
'blacklist_title_keywords': ['untitled', 'barbie'],
'allowed_countries': ['us', 'gb', 'ca']
}
}
}
cfg = None
class AttrConfig(AttrDict):
"""
Simple AttrDict subclass to return None when requested attribute does not exist
"""
def __init__(self, config):
super().__init__(config)
def __getattr__(self, item):
try:
return super().__getattr__(item)
except AttributeError:
pass
# Default behaviour
return None
def build_config():
if not os.path.exists(config_path):
print("Dumping default config to: %s" % config_path)
with open(config_path, 'w') as fp:
json.dump(base_config, fp, sort_keys=True, indent=2)
return True
else:
return False
def dump_config():
if os.path.exists(config_path):
with open(config_path, 'w') as fp:
json.dump(cfg, fp, sort_keys=True, indent=2)
return True
else:
return False
def load_config():
with open(config_path, 'r') as fp:
return AttrConfig(json.load(fp))
############################################################
# LOAD CFG
############################################################
# dump/load config
if build_config():
print("Please edit the default configuration before running again!")
exit(0)
else:
cfg = load_config()

@ -0,0 +1,313 @@
from misc import str as misc_str
from misc.log import logger
log = logger.get_logger(__name__)
############################################################
# SONARR
############################################################
def sonarr_series_to_tvdb_dict(sonarr_series):
series = {}
try:
for tmp in sonarr_series:
if 'tvdbId' not in tmp:
log.debug("Could not handle series: %s", tmp['title'])
continue
series[tmp['tvdbId']] = tmp
return series
except Exception:
log.exception("Exception processing sonarr series 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 series 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):
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:
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 before earliest_year %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['movie']['runtime'])
blacklisted = True
except Exception:
log.exception("Exception determining if show had sufficient runtime %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):
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
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):
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:
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 before earliest_year %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_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):
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
except Exception:
log.exception("Exception determining if movie was blacklisted %s: ", movie)
return blacklisted

@ -0,0 +1,50 @@
import logging
import os
import sys
from logging.handlers import RotatingFileHandler
from misc.config import cfg
class Logger:
def __init__(self, file_name=None, log_level=logging.DEBUG,
log_format='%(asctime)s - %(levelname)-10s - %(name)-35s - %(funcName)-35s - %(message)s'):
self.log_format = log_format
# init root_logger
self.log_formatter = logging.Formatter(log_format)
self.root_logger = logging.getLogger()
self.root_logger.setLevel(log_level)
# disable bloat loggers
logging.getLogger('urllib3').setLevel(logging.ERROR)
# init console_logger
self.console_handler = logging.StreamHandler(sys.stdout)
self.console_handler.setFormatter(self.log_formatter)
self.root_logger.addHandler(self.console_handler)
# init file_logger
if file_name:
if os.path.sep not in file_name:
# file_name was a filename, lets build a full file_path
self.log_file_path = os.path.join(os.path.dirname(sys.argv[0]), file_name)
else:
self.log_file_path = file_name
self.file_handler = RotatingFileHandler(
self.log_file_path,
maxBytes=1024 * 1024 * 5,
backupCount=5
)
self.file_handler.setFormatter(self.log_formatter)
self.root_logger.addHandler(self.file_handler)
# Set chosen logging level
self.root_logger.setLevel(log_level)
def get_logger(self, name):
return self.root_logger.getChild(name)
logger = Logger('activity.log', logging.DEBUG if cfg.core.debug else logging.INFO)

@ -0,0 +1,15 @@
from misc.log import logger
log = logger.get_logger(__name__)
def get_year_from_timestamp(timestamp):
year = 0
try:
if not timestamp:
return 0
year = timestamp[:timestamp.index('-')]
except Exception:
log.exception("Exception parsing year from %s: ", timestamp)
return int(year) if str(year).isdigit() else 0

@ -0,0 +1,4 @@
attrdict==2.0.0
click==6.7
backoff==1.4.3
requests==2.18.4

@ -0,0 +1,229 @@
#!/usr/bin/env python3
import time
import click
from media.radarr import Radarr
from media.sonarr import Sonarr
from media.trakt import Trakt
from misc import helpers
from misc.config import cfg
from misc.log import logger
############################################################
# INIT
############################################################
# Logging
log = logger.get_logger('traktarr')
# Click
@click.group(help='Add new series/movies to Sonarr & Radarr from Trakt.')
def app():
pass
############################################################
# SHOWS
############################################################
@app.command(help='Add new series to Sonarr.')
@click.option('--list-type', '-t', type=click.Choice(['anticipated', 'trending', 'popular']),
help='Trakt list to process.', required=True)
@click.option('--add-limit', '-l', default=0, help='Limit number of series added to Sonarr.', show_default=True)
@click.option('--add-delay', '-d', default=2.5, help='Seconds between each add request to Sonarr.', show_default=True)
def shows(list_type, add_limit=0, add_delay=2.5):
added_shows = 0
# validate trakt api_key
trakt = Trakt(cfg.trakt.api_key)
if not trakt.validate_api_key():
log.error("Aborting due to failure to validate Trakt API Key")
return
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
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
else:
log.info("Retrieved Profile ID for %s: %d", cfg.sonarr.profile, profile_id)
# get sonarr series list
sonarr_series_list = sonarr.get_series()
if not sonarr_series_list:
log.error("Aborting due to failure to retrieve Sonarr series list")
return
else:
log.info("Retrieved Sonarr series list, series found: %d", len(sonarr_series_list))
# get trakt series list
trakt_series_list = None
if list_type.lower() == 'anticipated':
trakt_series_list = trakt.get_anticipated_shows()
elif list_type.lower() == 'trending':
trakt_series_list = trakt.get_trending_shows()
elif list_type.lower() == 'popular':
trakt_series_list = trakt.get_popular_shows()
else:
log.error("Aborting due to unknown Trakt list type")
return
if not trakt_series_list:
log.error("Aborting due to failure to retrieve Trakt %s series list", list_type)
return
else:
log.info("Retrieved Trakt %s series list, series found: %d", list_type, len(trakt_series_list))
# build filtered series list without series that exist in sonarr
processed_series_list = helpers.sonarr_remove_existing_series(sonarr_series_list, trakt_series_list)
if not processed_series_list:
log.error("Aborting due to failure to remove existing Sonarr series from retrieved Trakt series list")
return
else:
log.info("Removed existing Sonarr series from Trakt series list, series left to process: %d",
len(processed_series_list))
# loop series_list
log.info("Processing list now...")
for series in processed_series_list:
try:
# check if series passes out blacklist criteria inspection
if not helpers.trakt_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())
# add show to sonarr
if sonarr.add_series(series['show']['ids']['tvdb'], series['show']['title'], profile_id,
cfg.sonarr.root_folder,
True):
log.info("ADDED %s (%d)", series['show']['title'], series['show']['year'])
added_shows += 1
else:
log.error("FAILED adding %s (%d)", series['show']['title'], series['show']['year'])
# stop adding shows, if added_shows >= add_limit
if add_limit and added_shows >= add_limit:
break
# sleep before adding any more
time.sleep(add_delay)
except Exception:
log.exception("Exception while processing series %s: ", series['show']['title'])
log.info("Added %d new shows to Sonarr", added_shows)
@app.command(help='Add new movies to Radarr.')
@click.option('--list-type', '-t', type=click.Choice(['anticipated', 'trending', 'popular']),
help='Trakt list to process.', required=True)
@click.option('--add-limit', '-l', default=0, help='Limit number of 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)
def movies(list_type, add_limit=0, add_delay=2.5):
added_movies = 0
# validate trakt api_key
trakt = Trakt(cfg.trakt.api_key)
if not trakt.validate_api_key():
log.error("Aborting due to failure to validate Trakt API Key")
return
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
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)
return
else:
log.info("Retrieved Profile ID for %s: %d", cfg.radarr.profile, profile_id)
# 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")
return
else:
log.info("Retrieved Radarr movies list, movies found: %d", len(radarr_movie_list))
# get trakt movies list
trakt_movies_list = None
if list_type.lower() == 'anticipated':
trakt_movies_list = trakt.get_anticipated_movies()
elif list_type.lower() == 'trending':
trakt_movies_list = trakt.get_trending_movies()
elif list_type.lower() == 'popular':
trakt_movies_list = trakt.get_popular_movies()
else:
log.error("Aborting due to unknown Trakt list type")
return
if not trakt_movies_list:
log.error("Aborting due to failure to retrieve Trakt %s movies list", list_type)
return
else:
log.info("Retrieved Trakt %s movies list, movies found: %d", list_type, len(trakt_movies_list))
# build filtered series list without series that exist in sonarr
processed_movies_list = helpers.radarr_remove_existing_movies(radarr_movie_list, trakt_movies_list)
if not processed_movies_list:
log.error("Aborting due to failure to remove existing Radarr movies from retrieved Trakt movies list")
return
else:
log.info("Removed existing Radarr movies from Trakt movies list, movies left to process: %d",
len(processed_movies_list))
# loop movies
log.info("Processing list now...")
for movie in processed_movies_list:
try:
# check if movie passes out blacklist criteria inspection
if not helpers.trakt_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
if radarr.add_movie(movie['movie']['ids']['tmdb'], movie['movie']['title'], movie['movie']['year'],
profile_id, cfg.sonarr.root_folder, True):
log.info("ADDED %s (%d)", movie['movie']['title'], movie['movie']['year'])
added_movies += 1
else:
log.error("FAILED adding %s (%d)", movie['movie']['title'], movie['movie']['year'])
# stop adding movies, if added_movies >= add_limit
if add_limit and added_movies >= add_limit:
break
# sleep before adding any more
time.sleep(add_delay)
except Exception:
log.exception("Exception while processing movie %s: ", movie['movie']['title'])
log.info("Added %d new movies to Radarr", added_movies)
############################################################
# MAIN
############################################################
if __name__ == "__main__":
app()
Loading…
Cancel
Save