Merge pull request #29 from l3uddz/develop

Develop
pull/48/head
James 7 years ago committed by GitHub
commit e6ac7323af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

3
.gitignore vendored

@ -29,6 +29,9 @@ __pycache__/
# Pyenv
**/.python-version
# Venv
venv/
# PyInstaller
build/
dist/

@ -1,5 +1,12 @@
[![made-with-python](https://img.shields.io/badge/Made%20with-Python-blue.svg)](https://www.python.org/)
[![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://github.com/l3uddz/traktarr/blob/master/LICENSE)
[![Feature Requests](https://img.shields.io/badge/Requests-Feathub-blue.svg)](http://feathub.com/l3uddz/traktarr)
[![Discord](https://img.shields.io/discord/381077432285003776.svg)](https://discord.gg/xmNYmSJ)
# traktarr
Add new shows & movies to Sonarr/Radarr from Trakt.
**traktarr** uses Trakt to add new shows into Sonarr and new movies into Radarr.
Types of Trakt lists supported:
@ -11,7 +18,7 @@ Types of Trakt lists supported:
- Anticipated
- boxoffice
- Boxoffice
- Public lists
@ -23,6 +30,53 @@ Types of Trakt lists supported:
\* Support for multiple (authenticated) users.
---
<!-- TOC depthFrom:1 depthTo:6 withLinks:1 updateOnSave:1 orderedList:0 -->
- [traktarr](#traktarr)
- [Demo](#demo)
- [Requirements](#requirements)
- [Installation](#installation)
- [1. Base Install](#1-base-install)
- [2. Create a Trakt Application](#2-create-a-trakt-application)
- [3. Authenticate User(s) (optional)](#3-authenticate-users-optional)
- [Configuration](#configuration)
- [Sample Configuration](#sample-configuration)
- [Core](#core)
- [Automatic](#automatic)
- [Personal Watchlists](#personal-watchlists)
- [Custom Lists](#custom-lists)
- [Public Lists](#public-lists)
- [Private Lists](#private-lists)
- [Filters](#filters)
- [Movies](#movies)
- [Shows](#shows)
- [Notifications](#notifications)
- [Pushover](#pushover)
- [Slack](#slack)
- [Radarr](#radarr)
- [Sonarr](#sonarr)
- [Tags](#tags)
- [Trakt](#trakt)
- [Usage](#usage)
- [Automatic (Scheduled)](#automatic-scheduled)
- [Setup](#setup)
- [Customize](#customize)
- [Manual (CLI)](#manual-cli)
- [General](#general)
- [Movie (Single Movie)](#movie-single-movie)
- [Movies (Multiple Movies)](#movies-multiple-movies)
- [Show (Single Show)](#show-single-show)
- [Shows (Multiple Shows)](#shows-multiple-shows)
- [Examples (Manual)](#examples-manual)
<!-- /TOC -->
---
# Demo
@ -33,8 +87,11 @@ Click to enlarge.
# Requirements
1. Python 3.5 or higher (`sudo apt install python3 python3-pip`).
2. requirements.txt modules (see below).
1. Ubuntu/Debian
2. Python 3.5 or higher (`sudo apt install python3 python3-pip`).
3. requirements.txt modules (see below).
# Installation
@ -43,20 +100,31 @@ Click to enlarge.
Install traktarr to be run with `traktarr` command.
1. `cd /opt`
2. `sudo git clone https://github.com/l3uddz/traktarr`
3. `sudo chown -R user:group traktarr` (run `id` to find your user / group)
4. `cd traktarr`
5. `sudo python3 -m pip install -r requirements.txt`
6. `sudo ln -s /opt/traktarr/traktarr.py /usr/local/bin/traktarr`
7. `traktarr` - run once to generate a sample a config.json file.
8. `nano config.json` - edit preferences.
## 2. Create a Trakt Application
1. Create a Trakt application by going [here](https://trakt.tv/oauth/applications/new)
2. Enter a name for your application; for example `traktarr`
3. Enter `urn:ietf:wg:oauth:2.0:oob` in the `Redirect uri` field.
4. Click "SAVE APP".
5. Open the traktarr configuration file `config.json` and insert the Client ID in the `client_id` and the Client Secret in the `client_secret`, like this:
```
@ -73,7 +141,9 @@ Install traktarr to be run with `traktarr` command.
For each user you want to access the private lists for (i.e. watchlist and/or custom lists), you will need to to authenticate that user.
Repeat the following steps for every user you want to authenticate:
1. Run `traktarr trakt_authentication`
2. You wil get the following prompt:
```
@ -81,13 +151,19 @@ Repeat the following steps for every user you want to authenticate:
- Go to: https://trakt.tv/activate on any device and enter A0XXXXXX. We'll be polling Trakt every 5 seconds for a reply
```
3. Go to https://trakt.tv/activate.
4. Enter the code you see in your terminal.
5. Click continue.
6. If you are not logged in to Trakt, login now.
7. Click "Accept".
8. You will get the message: "Woohoo! Your device is now connected and will automatically refresh in a few seconds.".
You've now authenticated the user.
You can repeat this process for as many users as you like.
@ -122,6 +198,7 @@ You can repeat this process for as many users as you like.
"gb",
"ca"
],
"allowed_languages": [],
"blacklist_title_keywords": [
"untitled",
"barbie",
@ -143,6 +220,7 @@ You can repeat this process for as many users as you like.
"gb",
"ca"
],
"allowed_languages": [],
"blacklisted_genres": [
"animation",
"game-show",
@ -317,45 +395,45 @@ Private lists can be added in two ways:
1. If there is only one authenticated user, you can add the private list just like any other public list:
```json
"automatic": {
"movies": {
"lists": {
"https://trakt.tv/users/user/lists/my-private-movies-list": 10
}
},
"shows": {
"lists": {
"https://trakt.tv/users/user/lists/my-private-shows-list": 10
}
}
},
```
```json
"automatic": {
"movies": {
"lists": {
"https://trakt.tv/users/user/lists/my-private-movies-list": 10
}
},
"shows": {
"lists": {
"https://trakt.tv/users/user/lists/my-private-shows-list": 10
}
}
},
```
2. If there are multiple authenticated users you want to fetch the lists from, you'll need to specify the username under `authenticate_as`.
_Note: The user should have access to the list (either own the list or a list that was shared to them by a friend)._
_Note: The user should have access to the list (either own the list or a list that was shared to them by a friend)._
```json
"automatic": {
"movies": {
"lists": {
"https://trakt.tv/users/user/lists/my-private-movies-list": {
"authenticate_as": "user2",
"limit": 10
}
}
},
"shows": {
"lists": {
"https://trakt.tv/users/user/lists/my-private-shows-list": {
"authenticate_as": "user2",
"limit": 10
}
}
}
},
```
```json
"automatic": {
"movies": {
"lists": {
"https://trakt.tv/users/user/lists/my-private-movies-list": {
"authenticate_as": "user2",
"limit": 10
}
}
},
"shows": {
"lists": {
"https://trakt.tv/users/user/lists/my-private-shows-list": {
"authenticate_as": "user2",
"limit": 10
}
}
}
},
```
## Filters
@ -371,6 +449,7 @@ Use filters to specify the movie/shows's country of origin or blacklist (i.e. fi
"gb",
"ca"
],
"allowed_languages": [],
"blacklist_title_keywords": [
"untitled",
"barbie"
@ -387,7 +466,12 @@ Use filters to specify the movie/shows's country of origin or blacklist (i.e. fi
},
```
`allowed_countries` - allowed countries of origin.
`allowed_countries` - only add movies from these countries.
`allowed_languages` - only add movies with these languages (default/blank=English).
- By default, traktarr will only query shows in English. If you need to search for other languages (e.g. Japanese for anime), you must add those languages here.
- Languages are in [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) format (e.g. `ja` for Japanese.)
`blacklist_title_keywords` - blacklist certain words in titles.
@ -410,6 +494,7 @@ Use filters to specify the movie/shows's country of origin or blacklist (i.e. fi
"gb",
"ca"
],
"allowed_languages": [],
"blacklisted_genres": [
"animation",
"game-show",
@ -445,7 +530,12 @@ Use filters to specify the movie/shows's country of origin or blacklist (i.e. fi
}
```
`allowed_countries` - allowed countries of origin.
`allowed_countries` - only add shows from these countries.
`allowed_languages` - only add shows with these languages (default/blank=English).
- By default, traktarr will only query shows in English. If you need to search for other languages (e.g. Japanese for anime), you must add those languages here.
- Languages are in [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) format (e.g. `ja` for Japanese.)
`blacklisted_genres` - blacklist certain genres.
@ -551,49 +641,46 @@ Sonarr configuration.
### Tags
To show how tags work, we will create a sample tag `AMZN` and assign it to certain networks.
_Note: These are optional._
### Sonarr
The `tags` option allows Sonarr to assign tags to shows from specific television networks, so that Sonarr can filter in/out certain keywords from releases.
First, we will create a tag in Sonarr (Settings > Indexers > Restrictions).
```
Must contain: BluRay, Amazon, AMZN,
Must not contain:
Tags: AMZN
```
**Example:**
### traktarr
To show how tags work, we will create a tag `AMZN` and assign it to certain television networks that usually have AMZN releases.
Finally, we will edit the traktarr config and assign the `AMZN` tag to certain networks.
1. First, we will create a tag in Sonarr (Settings > Indexers > Restrictions).
```json
"tags": {
"amzn": [
"hbo",
"amc",
"usa network",
"tnt",
"starz",
"the cw",
"fx",
"fox",
"abc",
"nbc",
"cbs",
"tbs",
"amazon",
"syfy",
"cinemax",
"bravo",
"showtime",
"paramount network"
]
}
```
Must contain: BluRay, Amazon, AMZN
Must not contain:
Tags: AMZN
```
```
2. And, finally, we will edit the traktarr config and assign the `AMZN` tag to some networks.
```json
"tags": {
"amzn": [
"hbo",
"amc",
"usa network",
"tnt",
"starz",
"the cw",
"fx",
"fox",
"abc",
"nbc",
"cbs",
"tbs",
"amazon",
"syfy",
"cinemax",
"bravo",
"showtime",
"paramount network"
]
}
```
## Trakt
@ -615,14 +702,41 @@ Trakt Authentication info:
## Automatic (Scheduled)
To have traktarr get Movies and Shows for you automatically, on set interval.
### Setup
To have traktarr get Movies and Shows for you automatically, on set interval, do the following:
1. `sudo cp /opt/traktarr/systemd/traktarr.service /etc/systemd/system/`
2. `sudo nano /etc/systemd/system/traktarr.service` and edit user/group to match yours.
3. `sudo systemctl daemon-reload`
4. `sudo systemctl enable traktarr.service`
5. `sudo systemctl start traktarr.service`
### Customize
You can customize how the scheduled traktarr is ran by editing the `traktarr.service` file and adding any of the following options:
```
-d, --add-delay FLOAT Seconds between each add request to Sonarr / Radarr.
[default: 2.5]
--no-search Disable search when adding to Sonarr / Radarr.
--run-now Do a first run immediately without waiting.
--no-notifications Disable notifications.
--help Show this message and exit.
```
You can bring up the list, anytime, by running the following command:
```
traktarr run --help
```
\* Remember, other configuration options need to go into the `config.json` file under the `Automatic` section.
## Manual (CLI)
### General
@ -669,7 +783,7 @@ Options:
--help Show this message and exit.
```
_Note: This command only works with `-id` or `--show_id` specified (i.e. not with lists), and support both Trakt IDs and IMDB IDs._
_Note: This command only works with `-id` or `--show_id` specified (i.e. not with lists), and supports both Trakt IDs and IMDB IDs._
### Movies (Multiple Movies)
@ -720,7 +834,7 @@ Options:
--help Show this message and exit.
```
_Note: This command only works with `-id` or `--show_id` specified (i.e. not with lists), and support both Trakt IDs and IMDB IDs._
_Note: This command only works with `-id` or `--show_id` specified (i.e. not with lists), and supports both Trakt IDs and IMDB IDs._
### Shows (Multiple Shows)

@ -0,0 +1,65 @@
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
def unblacklist_genres(genre, blacklisted_genres):
genres = genre.split(',')
for allow_genre in genres:
if allow_genre in blacklisted_genres:
blacklisted_genres.remove(allow_genre)
return
def allowed_genres(genre, object_type, trakt_object):
allowed_object = False
genres = genre.split(',')
for item in genres:
if item.lower() in trakt_object[object_type]['genres']:
allowed_object = True
break
return allowed_object

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

@ -60,6 +60,7 @@ class Config(object, metaclass=Singleton):
'blacklisted_genres': [],
'blacklisted_networks': [],
'allowed_countries': [],
'allowed_languages': [],
'blacklisted_min_runtime': 15,
'blacklisted_min_year': 2000,
'blacklisted_max_year': 2019,
@ -72,7 +73,8 @@ class Config(object, metaclass=Singleton):
'blacklisted_max_year': 2019,
'blacklist_title_keywords': [],
'blacklisted_tmdb_ids': [],
'allowed_countries': []
'allowed_countries': [],
'allowed_languages': []
}
},
'automatic': {
@ -154,11 +156,6 @@ class Config(object, metaclass=Singleton):
sub_upgraded = False
merged = settings2.copy()
# print(settings1)
# print(settings2)
# print(overwrite)
# print("_______________")
if isinstance(settings1, dict):
for k, v in settings1.items():
# missing k

@ -1,5 +1,6 @@
#!/usr/bin/env python3
import os.path
import signal
import sys
import time
@ -16,7 +17,7 @@ notify = None
# Click
@click.group(help='Add new shows & movies to Sonarr/Radarr from Trakt.')
@click.version_option('1.2.0', prog_name='traktarr')
@click.version_option('1.2.1', prog_name='traktarr')
@click.option(
'--config',
envvar='TRAKTARR_CONFIG',
@ -62,13 +63,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 +129,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 +155,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,13 +185,15 @@ def shows(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_sea
authenticate_user=None):
from media.sonarr import Sonarr
from media.trakt import Trakt
from misc import helpers
from helpers import misc as misc_helper
from helpers import sonarr as sonarr_helper
from helpers import trakt as trakt_helper
added_shows = 0
# remove genre from shows blacklisted_genres if supplied
if genre and genre in cfg.filters.shows.blacklisted_genres:
cfg['filters']['shows']['blacklisted_genres'].remove(genre)
if genre:
misc_helper.unblacklist_genres(genre, cfg['filters']['shows']['blacklisted_genres'])
# replace sonarr root_folder if folder is supplied
if folder:
@ -173,73 +201,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(genres=genre, languages=cfg.filters.shows.allowed_languages)
elif list_type.lower() == 'trending':
trakt_series_list = trakt.get_trending_shows()
trakt_objects_list = trakt.get_trending_shows(genres=genre, languages=cfg.filters.shows.allowed_languages)
elif list_type.lower() == 'popular':
trakt_series_list = trakt.get_popular_shows()
trakt_objects_list = trakt.get_popular_shows(genres=genre, languages=cfg.filters.shows.allowed_languages)
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 +231,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:
@ -271,31 +255,31 @@ def shows(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_sea
for series in sorted_series_list:
try:
# check if genre matches genre supplied via argument
if genre and genre.lower() not in series['show']['genres']:
log.debug("Skipping: %s because it was not from %s genre", series['show']['title'], genre.lower())
if genre and not misc_helper.allowed_genres(genre, 'show', series):
log.debug("Skipping: %s because it was not from %s genre(s)", series['show']['title'], genre.lower())
continue
# check if series passes out blacklist criteria inspection
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 +318,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,13 +362,15 @@ def movies(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_se
authenticate_user=None):
from media.radarr import Radarr
from media.trakt import Trakt
from misc import helpers
from helpers import misc as misc_helper
from helpers import radarr as radarr_helper
from helpers import trakt as trakt_helper
added_movies = 0
# remove genre from movies blacklisted_genres if supplied
if genre and genre in cfg.filters.movies.blacklisted_genres:
cfg['filters']['movies']['blacklisted_genres'].remove(genre)
if genre:
misc_helper.unblacklist_genres(genre, cfg['filters']['movies']['blacklisted_genres'])
# replace radarr root_folder if folder is supplied
if folder:
@ -406,65 +378,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(genres=genre, languages=cfg.filters.movies.allowed_languages)
elif list_type.lower() == 'trending':
trakt_movies_list = trakt.get_trending_movies()
trakt_objects_list = trakt.get_trending_movies(genres=genre, languages=cfg.filters.movies.allowed_languages)
elif list_type.lower() == 'popular':
trakt_movies_list = trakt.get_popular_movies()
trakt_objects_list = trakt.get_popular_movies(genres=genre, languages=cfg.filters.movies.allowed_languages)
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 +409,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:
@ -496,12 +433,12 @@ def movies(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_se
for movie in sorted_movies_list:
try:
# check if genre matches genre supplied via argument
if genre and genre.lower() not in movie['movie']['genres']:
log.debug("Skipping: %s because it was not from %s genre", movie['movie']['title'], genre.lower())
if genre and not misc_helper.allowed_genres(genre, 'movie', movie):
log.debug("Skipping: %s because it was not from %s genre(s)", movie['movie']['title'], genre.lower())
continue
# check if movie passes out blacklist criteria inspection
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 +491,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
@ -705,28 +645,34 @@ def automatic_movies(add_delay=2.5, no_search=False, notifications=False):
@click.option('--add-delay', '-d', default=2.5, help='Seconds between each add request to Sonarr / Radarr.',
show_default=True)
@click.option('--no-search', is_flag=True, help='Disable search when adding to Sonarr / Radarr.')
@click.option('--run-now', is_flag=True, help="Do a first run immediately without waiting.")
@click.option('--no-notifications', is_flag=True, help="Disable notifications.")
def run(add_delay=2.5, no_search=False, no_notifications=False):
def run(add_delay=2.5, no_search=False, run_now=False, no_notifications=False):
log.info("Automatic mode is now running...")
# Add tasks to schedule and do first run
# Add tasks to schedule and do first run if enabled
if cfg.automatic.movies.interval:
schedule.every(cfg.automatic.movies.interval).hours.do(
movie_schedule = schedule.every(cfg.automatic.movies.interval).hours.do(
automatic_movies,
add_delay,
no_search,
not no_notifications
).run()
)
if run_now:
movie_schedule.run()
# Sleep between tasks
time.sleep(add_delay)
if cfg.automatic.shows.interval:
schedule.every(cfg.automatic.shows.interval).hours.do(
shows_schedule = schedule.every(cfg.automatic.shows.interval).hours.do(
automatic_shows,
add_delay,
no_search,
not no_notifications
).run()
)
if run_now:
shows_schedule.run()
# Enter running schedule
while True:
@ -758,13 +704,19 @@ def init_notifications():
return
# Handles exit signals, cancels jobs and exits cleanly
def exit_handler(signum, frame):
log.info("Received %s, canceling jobs and exiting.", signal.Signals(signum).name)
schedule.clear()
exit()
############################################################
# MAIN
############################################################
if __name__ == "__main__":
print("""
,--. ,--. ,--.
,-' '-.,--.--. ,--,--.| |,-.,-' '-. ,--,--.,--.--.,--.--.
'-. .-'| .--'' ,-. || /'-. .-'' ,-. || .--'| .--'
@ -779,5 +731,11 @@ if __name__ == "__main__":
#########################################################################
# GNU General Public License v3.0 #
#########################################################################
""")
""")
# Register the signal handlers
signal.signal(signal.SIGTERM, exit_handler)
signal.signal(signal.SIGINT, exit_handler)
# Start application
app()

Loading…
Cancel
Save