From 32b20c025c63eb1b3728b762c60df9c4b048b965 Mon Sep 17 00:00:00 2001 From: "Nicholas St. Germain" Date: Wed, 28 Nov 2018 14:32:39 -0600 Subject: [PATCH] sonarr.py overhaul --- helpers.py | 42 ++++++ sonarr.py | 401 ++++++++++++++++++++++------------------------------- 2 files changed, 210 insertions(+), 233 deletions(-) create mode 100644 helpers.py diff --git a/helpers.py b/helpers.py new file mode 100644 index 0000000..c30773f --- /dev/null +++ b/helpers.py @@ -0,0 +1,42 @@ +from typing import NamedTuple + + +class TVShow(NamedTuple): + seriesId: int + episodeFileId: int + seasonNumber: int + episodeNumber: int + title: str + airDate: str + airDateUtc: str + overview: str + episodeFile: dict + hasFile: bool + monitored: bool + unverifiedSceneNumbering: bool + absoluteEpisodeNumber: int + series: dict + id: int + + +class Queue(NamedTuple): + series: dict + episode: dict + quality: dict + size: float + title: str + sizeleft: float + timeleft: str + estimatedCompletionTime: str + status: str + trackedDownloadStatus: str + statusMessages: list + downloadId: str + protocol: str + id: int + + +class Server(NamedTuple): + url: str + api_key: str + id: int diff --git a/sonarr.py b/sonarr.py index f504b74..f62c0b4 100644 --- a/sonarr.py +++ b/sonarr.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 # Do not edit this script. Edit configuration.py import sys import requests @@ -5,257 +6,183 @@ from datetime import datetime, timezone, date, timedelta from influxdb import InfluxDBClient import argparse from argparse import RawTextHelpFormatter -import configuration +import configuration as config +from helpers import Server, TVShow, Queue -def now_iso(): - now = datetime.now(timezone.utc).astimezone().isoformat() - return now +class SonarrAPI(object): + TVShow.__new__.__defaults__ = (None,) * len(TVShow._fields) + def __init__(self): + self.now = datetime.now(timezone.utc).astimezone().isoformat() + self.today = str(date.today()) + self.influx = InfluxDBClient(config.influxdb_url, config.influxdb_port, config.influxdb_username, + config.influxdb_password, config.sonarr_influxdb_db_name) + self.influx_payload = [] + self.servers = self.get_servers() + self.session = requests.Session() + self.session.params = {'pageSize': 1000} -def influx_sender(influx_payload): - influx = InfluxDBClient(configuration.influxdb_url, configuration.influxdb_port, configuration.influxdb_username, - configuration.influxdb_password, configuration.sonarr_influxdb_db_name) - influx.write_points(influx_payload) + @staticmethod + def get_servers(): + if not config.sonarr_server_list: + sys.exit("No Sonarr servers defined in config") + servers = [] + for url, api_key, server_id in config.sonarr_server_list: + servers.append(Server(url=url, api_key=api_key, id=server_id)) -def get_all_missing_shows(): - # Set the time here so we have one timestamp to work with - now = now_iso() - missing, influx_payload = [], [] + return servers - for sonarr_url, sonarr_api_key, server_id in configuration.sonarr_server_list: + def get_missing(self, days_past): + endpoint = '/api/calendar' + last_days = str(date.today() + timedelta(days=-days_past)) + params = {'start': last_days, 'end': self.today} - headers = {'X-Api-Key': sonarr_api_key} + for server in self.servers: + missing = [] + headers = {'X-Api-Key': server.api_key} - get_tv_shows = requests.get('{}/api/wanted/missing/?pageSize=1000'.format(sonarr_url), - headers=headers).json()['records'] + get = self.session.get(server.url + endpoint, params=params, headers=headers).json() + tv_shows = [TVShow(**show) for show in get] - tv_shows = {d['id']: d for d in get_tv_shows} + for show in tv_shows: + if not show.hasFile: + sxe = 'S{:0>2}E{:0>2}'.format(show.seasonNumber, show.episodeNumber) + missing.append((show.series['title'], sxe, show.airDate, show.title, show.id)) - for show in tv_shows.keys(): - series_title = '{}'.format(tv_shows[show]['series']['title']) - sxe = 'S{:0>2}E{:0>2}'.format(tv_shows[show]['seasonNumber'], tv_shows[show]['episodeNumber']) - missing.append((series_title, sxe, tv_shows[show]['id'], tv_shows[show]['title'])) - - for series_title, sxe, sonarr_id, episode_title in missing: - influx_payload.append( - { - "measurement": "Sonarr", - "tags": { - "type": "Missing", - "sonarrId": sonarr_id, - "server": server_id - }, - "time": now, - "fields": { - "name": series_title, - "epname": episode_title, - "sxe": sxe + for series_title, sxe, air_date, episode_title, sonarr_id in missing: + self.influx_payload.append( + { + "measurement": "Sonarr", + "tags": { + "type": "Missing", + "sonarrId": sonarr_id, + "server": server.id + }, + "time": self.now, + "fields": { + "name": series_title, + "epname": episode_title, + "sxe": sxe, + "airs": air_date + } } - } - ) - # Empty missing or else things get foo bared - missing = [] + ) - return influx_payload + def get_upcoming(self): + endpoint = '/api/calendar/' + for server in self.servers: + upcoming = [] + headers = {'X-Api-Key': server.api_key} -def get_missing_shows(days_past): - # Set the time here so we have one timestamp to work with - now = now_iso() - last_days = str(date.today()+timedelta(days=-days_past)) - today = str(date.today()) - missing, influx_payload = [], [] + get = self.session.get(server.url + endpoint, headers=headers).json() + tv_shows = [TVShow(**show) for show in get] - for sonarr_url, sonarr_api_key, server_id in configuration.sonarr_server_list: + for show in tv_shows: + sxe = 'S{:0>2}E{:0>2}'.format(show.seasonNumber, show.episodeNumber) + upcoming.append((show.series['title'], sxe, show.id, show.title, show.airDate)) - headers = {'X-Api-Key': sonarr_api_key} - - get_tv_shows = requests.get('{}/api/calendar/?start={}&end={}&pageSize=1000'.format(sonarr_url, last_days, - today), - headers=headers).json() - - tv_shows = {d['id']: d for d in get_tv_shows} - - for show in tv_shows.keys(): - if not (tv_shows[show]['hasFile']): - series_title = '{}'.format(tv_shows[show]['series']['title']) - sxe = 'S{:0>2}E{:0>2}'.format(tv_shows[show]['seasonNumber'], tv_shows[show]['episodeNumber']) - air_date = (tv_shows[show]['airDate']) - missing.append((series_title, sxe, air_date, tv_shows[show]['id'])) - - for series_title, sxe, air_date, sonarr_id in missing: - influx_payload.append( - { - "measurement": "Sonarr", - "tags": { - "type": "Missing_Days", - "sonarrId": sonarr_id, - "server": server_id - }, - "time": now, - "fields": { - "name": series_title, - "sxe": sxe, - "airs": air_date + for series_title, sxe, sonarr_id, episode_title, air_date in upcoming: + self.influx_payload.append( + { + "measurement": "Sonarr", + "tags": { + "type": "Soon", + "sonarrId": sonarr_id, + "server": server.id + }, + "time": self.now, + "fields": { + "name": series_title, + "epname": episode_title, + "sxe": sxe, + "airs": air_date + } } - } - ) + ) - # Empty missing or else things get foo bared - missing = [] + def get_future(self, future_days): + endpoint = '/api/calendar/' + future = str(date.today() + timedelta(days=future_days)) - return influx_payload + for server in self.servers: + air_days = [] + headers = {'X-Api-Key': server.api_key} + params = {'start': self.today, 'end': future} + get = self.session.get(server.url + endpoint, params=params, headers=headers).json() + tv_shows = [TVShow(**show) for show in get] -def get_upcoming_shows(): - # Set the time here so we have one timestamp to work with - now = now_iso() - upcoming = [] - influx_payload = [] + for show in tv_shows: + sxe = 'S{:0>2}E{:0>2}'.format(show.seasonNumber, show.episodeNumber) + air_days.append((show.series['title'], show.hasFile, sxe, show.title, show.airDate, show.id)) - for sonarr_url, sonarr_api_key, server_id in configuration.sonarr_server_list: - - headers = {'X-Api-Key': sonarr_api_key} - - upcoming_shows_request = requests.get('{}/api/calendar/'.format(sonarr_url), headers=headers).json() - - upcoming_shows = {d['id']: d for d in upcoming_shows_request} - - for show in upcoming_shows.keys(): - series_title = '{}'.format(upcoming_shows[show]['series']['title']) - sxe = 'S{:0>2}E{:0>2}'.format(upcoming_shows[show]['seasonNumber'], upcoming_shows[show]['episodeNumber']) - upcoming.append((series_title, sxe, upcoming_shows[show]['id'], upcoming_shows[show]['title'], - upcoming_shows[show]['airDate'])) - - for series_title, sxe, sonarr_id, episode_title, air_date in upcoming: - influx_payload.append( - { - "measurement": "Sonarr", - "tags": { - "type": "Soon", - "sonarrId": sonarr_id, - "server": server_id - }, - "time": now, - "fields": { - "name": series_title, - "epname": episode_title, - "sxe": sxe, - "airs": air_date + for series_title, dl_status, sxe, episode_title, air_date, sonarr_id in air_days: + self.influx_payload.append( + { + "measurement": "Sonarr", + "tags": { + "type": "Future", + "sonarrId": sonarr_id, + "server": server.id + }, + "time": self.now, + "fields": { + "name": series_title, + "epname": episode_title, + "sxe": sxe, + "airs": air_date, + "downloaded": dl_status + } } - } - ) - # Empty upcoming or else things get foo bared - upcoming = [] + ) - return influx_payload + def get_queue(self): + endpoint = '/api/queue' + for server in self.servers: + queue = [] + headers = {'X-Api-Key': server.api_key} -def get_future_shows(future_days): - # Set the time here so we have one timestamp to work with - now = now_iso() + get = self.session.get(server.url + endpoint, headers=headers).json() + download_queue = [Queue(**show) for show in get] - today = str(date.today()) - future = str(date.today()+timedelta(days=future_days)) - air_days = [] - influx_payload = [] + for show in download_queue: + sxe = 'S{:0>2}E{:0>2}'.format(show.episode['seasonNumber'], show.episode['episodeNumber']) + if show.protocol.upper() == 'USENET': + protocol_id = 1 + else: + protocol_id = 0 - for sonarr_url, sonarr_api_key, server_id in configuration.sonarr_server_list: + queue.append((show.series['title'], show.episode['title'], show.protocol.upper(), + protocol_id, sxe, show.id)) - headers = {'X-Api-Key': sonarr_api_key} + for series_title, episode_title, protocol, protocol_id, sxe, sonarr_id in queue: + self.influx_payload.append( + { + "measurement": "Sonarr", + "tags": { + "type": "Queue", + "sonarrId": sonarr_id, + "server": server.id - get_tv_shows = requests.get('{}/api/calendar/?start={}&end={}&pageSize=200'.format(sonarr_url, today, future), - headers=headers).json() - - tv_shows = {d['id']: d for d in get_tv_shows} - - for show in tv_shows.keys(): - series_title = '{}'.format(tv_shows[show]['series']['title']) - dl_status = int(tv_shows[show]['hasFile']) - sxe = 'S{:0>2}E{:0>2}'.format(tv_shows[show]['seasonNumber'], tv_shows[show]['episodeNumber']) - air_days.append((series_title, dl_status, sxe, tv_shows[show]['title'], tv_shows[show]['airDate'], - tv_shows[show]['id'])) - - for series_title, dl_status, sxe, episode_title, air_date, sonarr_id in air_days: - influx_payload.append( - { - "measurement": "Sonarr", - "tags": { - "type": "Future", - "sonarrId": sonarr_id, - "server": server_id - }, - "time": now, - "fields": { - "name": series_title, - "epname": episode_title, - "sxe": sxe, - "airs": air_date, - "downloaded": dl_status + }, + "time": self.now, + "fields": { + "name": series_title, + "epname": episode_title, + "sxe": sxe, + "protocol": protocol, + "protocol_id": protocol_id + } } - } - ) - # Empty air_days or else things get foo bared - air_days = [] + ) - return influx_payload - - -def get_queue_shows(): - # Set the time here so we have one timestamp to work with - now = now_iso() - queue = [] - influx_payload = [] - - for sonarr_url, sonarr_api_key, server_id in configuration.sonarr_server_list: - - headers = {'X-Api-Key': sonarr_api_key} - - get_tv_shows = requests.get('{}/api/queue'.format(sonarr_url), - headers=headers).json() - - tv_shows = {d['id']: d for d in get_tv_shows} - - for show in tv_shows.keys(): - series_title = '{}'.format(tv_shows[show]['series']['title']) - episode_title = '{}'.format(tv_shows[show]['episode']['title']) - protocol = tv_shows[show]['protocol'].upper() - sxe = 'S{:0>2}E{:0>2}'.format(tv_shows[show]['episode']['seasonNumber'], - tv_shows[show]['episode']['episodeNumber']) - if protocol == 'USENET': - protocol_id = 1 - else: - protocol_id = 0 - - queue.append((series_title, episode_title, protocol, protocol_id, sxe, tv_shows[show]['id'])) - - for series_title, episode_title, protocol, protocol_id, sxe, sonarr_id in queue: - influx_payload.append( - { - "measurement": "Sonarr", - "tags": { - "type": "Queue", - "sonarrId": sonarr_id, - "server": server_id - - }, - "time": now, - "fields": { - "name": series_title, - "epname": episode_title, - "sxe": sxe, - "protocol": protocol, - "protocol_id": protocol_id - } - } - ) - - # Empty queue or else things get foo bared - queue = [] - - return influx_payload + def influx_push(self): + # TODO: error handling for failed connection + self.influx.write_points(self.influx_payload) if __name__ == "__main__": @@ -263,25 +190,33 @@ if __name__ == "__main__": description='Script to aid in data gathering from Sonarr', formatter_class=RawTextHelpFormatter) - parser.add_argument("--missing", action='store_true', help='Get all missing TV shows') - parser.add_argument("--missing_days", type=int, help='Get missing TV shows in past X days') + parser.add_argument("--missing", metavar='$days', type=int, help='Get missing TV shows in past X days' + '\ni.e. --missing 7 is in the last week') + parser.add_argument("--missing_days", metavar='$days', type=int, help='legacy command. Deprecated in favor of' + ' --missing' + '\nfunctions identically to --missing' + '\nNote: Will be removed in a future release') parser.add_argument("--upcoming", action='store_true', help='Get upcoming TV shows') - parser.add_argument("--future", type=int, help='Get TV shows on X days into the future. Includes today.' - '\ni.e. --future 2 is Today and Tomorrow') + parser.add_argument("--future", metavar='$days', type=int, help='Get TV shows on X days into the future. ' + 'Includes today.' + '\ni.e. --future 2 is Today and Tomorrow') parser.add_argument("--queue", action='store_true', help='Get TV shows in queue') opts = parser.parse_args() + sonarr = SonarrAPI() - if opts.missing: - influx_sender(get_all_missing_shows()) - elif opts.missing_days: - influx_sender(get_missing_shows(opts.missing_days)) - elif opts.upcoming: - influx_sender(get_upcoming_shows()) - elif opts.future: - influx_sender(get_future_shows(opts.future)) - elif opts.queue: - influx_sender(get_queue_shows()) - elif len(sys.argv) == 1: + if len(sys.argv) == 1: parser.print_help(sys.stderr) sys.exit(1) + + if any([opts.missing, opts.missing_days]): + days = opts.missing if opts.missing else opts.missing_days + sonarr.get_missing(days) + if opts.upcoming: + sonarr.get_upcoming() + if opts.future: + sonarr.get_future(opts.future) + if opts.queue: + sonarr.get_queue() + + sonarr.influx_push()