diff --git a/Varken.py b/Varken.py index 3641cbc..fedb080 100644 --- a/Varken.py +++ b/Varken.py @@ -14,6 +14,7 @@ from logging import getLogger, StreamHandler, Formatter, DEBUG # Needed to check version of python from varken import structures # noqa from varken.ombi import OmbiAPI +from varken.overseerr import OverseerrAPI from varken.unifi import UniFiAPI from varken import VERSION, BRANCH, BUILD_DATE from varken.sonarr import SonarrAPI @@ -156,6 +157,21 @@ if __name__ == "__main__": at_time = schedule.every(server.issue_status_run_seconds).seconds at_time.do(thread, OMBI.get_issue_counts).tag("ombi-{}-get_issue_counts".format(server.id)) + if CONFIG.overseerr_enabled: + for server in CONFIG.overseerr_servers: + OVERSEER = OverseerrAPI(server, DBMANAGER) + if server.get_request_total_counts: + at_time = schedule.every(server.request_total_run_seconds).seconds + at_time.do(thread, OVERSEER.get_total_requests).tag("overseerr-{}-get_total_requests".format(server.id)) + if server.get_request_status_counts: + at_time = schedule.every(server.request_status_run_seconds).seconds + at_time.do(thread, OVERSEER.get_request_status_counts).tag("overseerr-{}-get_request_status_counts" + .format(server.id)) + if server.get_latest_requests: + at_time = schedule.every(server.num_latest_requests_seconds).seconds + at_time.do(thread, OVERSEER.get_latest_requests).tag("overseerr-{}-get_latest_requests" + .format(server.id)) + if CONFIG.sickchill_enabled: for server in CONFIG.sickchill_servers: SICKCHILL = SickChillAPI(server, DBMANAGER) diff --git a/data/varken.example.ini b/data/varken.example.ini index fa072cf..e5eb650 100644 --- a/data/varken.example.ini +++ b/data/varken.example.ini @@ -3,7 +3,8 @@ sonarr_server_ids = 1,2 radarr_server_ids = 1,2 lidarr_server_ids = false tautulli_server_ids = 1 -ombi_server_ids = 1 +ombi_server_ids = false +overseerr_server_ids = 1 sickchill_server_ids = false unifi_server_ids = false maxmind_license_key = xxxxxxxxxxxxxxxx @@ -95,6 +96,19 @@ request_total_run_seconds = 300 get_issue_status_counts = true issue_status_run_seconds = 300 +[overseerr-1] +url = overseerr.domain.tld +apikey = xxxxxxxxxxxxxxxx +ssl = false +verify_ssl = false +get_request_total_counts = true +request_total_run_seconds = 300 +get_request_status_counts = true +request_status_run_seconds = 300 +get_latest_requests = true +num_latest_requests_to_fetch = 10 +num_latest_requests_seconds = 300 + [sickchill-1] url = sickchill.domain.tld:8081 apikey = xxxxxxxxxxxxxxxx diff --git a/varken/iniparser.py b/varken/iniparser.py index e241f31..4db95f1 100644 --- a/varken/iniparser.py +++ b/varken/iniparser.py @@ -9,7 +9,7 @@ from configparser import ConfigParser, NoOptionError, NoSectionError from varken.varkenlogger import BlacklistFilter from varken.structures import SickChillServer, UniFiServer from varken.helpers import clean_sid_check, rfc1918_ip_check, boolcheck -from varken.structures import SonarrServer, RadarrServer, OmbiServer, TautulliServer, InfluxServer +from varken.structures import SonarrServer, RadarrServer, OmbiServer, OverseerrServer, TautulliServer, InfluxServer class INIParser(object): @@ -17,7 +17,7 @@ class INIParser(object): self.config = None self.data_folder = data_folder self.filtered_strings = None - self.services = ['sonarr', 'radarr', 'lidarr', 'ombi', 'tautulli', 'sickchill', 'unifi'] + self.services = ['sonarr', 'radarr', 'lidarr', 'ombi', 'overseerr', 'tautulli', 'sickchill', 'unifi'] self.logger = getLogger() self.influx_server = InfluxServer() @@ -293,6 +293,38 @@ class INIParser(object): issue_status_counts=issue_status_counts, issue_status_run_seconds=issue_status_run_seconds) + if service == 'overseerr': + get_latest_requests = boolcheck(env.get( + f'VRKN_{envsection}_GET_LATEST_REQUESTS', + self.config.get(section, 'get_latest_requests'))) + num_latest_requests_to_fetch = int(env.get( + f'VRKN_{envsection}_NUM_LATEST_REQUESTS', + self.config.getint(section, 'num_latest_requests_to_fetch'))) + num_latest_requests_seconds = int(env.get( + f'VRKN_{envsection}_NUM_LATEST_REQUESTS_SECONDS', + self.config.getint(section, 'num_latest_requests_seconds'))) + get_request_total_counts = boolcheck(env.get( + f'VRKN_{envsection}_GET_REQUEST_TOTAL_COUNTS', + self.config.get(section, 'get_request_total_counts'))) + request_total_run_seconds = int(env.get( + f'VRKN_{envsection}_REQUEST_TOTAL_RUN_SECONDS', + self.config.getint(section, 'request_total_run_seconds'))) + get_request_status_counts = boolcheck(env.get( + f'VRKN_{envsection}_GET_REQUEST_STATUS_COUNTS', + self.config.get(section, 'get_request_status_counts'))) + request_status_run_seconds = int(env.get( + f'VRKN_{envsection}_REQUEST_STATUS_RUN_SECONDS', + self.config.getint(section, 'request_status_run_seconds'))) + + server = OverseerrServer(id=server_id, url=scheme + url, api_key=apikey, + verify_ssl=verify_ssl, get_latest_requests=get_latest_requests, + num_latest_requests_to_fetch=num_latest_requests_to_fetch, + num_latest_requests_seconds=num_latest_requests_seconds, + get_request_total_counts=get_request_total_counts, + request_total_run_seconds=request_total_run_seconds, + get_request_status_counts=get_request_status_counts, + request_status_run_seconds=request_status_run_seconds) + if service == 'sickchill': get_missing = boolcheck(env.get(f'VRKN_{envsection}_GET_MISSING', self.config.get(section, 'get_missing'))) diff --git a/varken/overseerr.py b/varken/overseerr.py new file mode 100644 index 0000000..55e8880 --- /dev/null +++ b/varken/overseerr.py @@ -0,0 +1,179 @@ +from logging import getLogger +from requests import Session, Request +from datetime import datetime, timezone + +from varken.helpers import connection_handler, hashit +from varken.structures import OverseerrRequest, OverseerrRequestCounts + + +class OverseerrAPI(object): + def __init__(self, server, dbmanager): + self.dbmanager = dbmanager + self.server = server + # Create session to reduce server web thread load, and globally define pageSize for all requests + self.session = Session() + self.session.headers = {'X-Api-Key': self.server.api_key} + self.logger = getLogger() + + def __repr__(self): + return f"" + + def get_total_requests(self): + now = datetime.now(timezone.utc).astimezone().isoformat() + endpoint = '/api/v1/request?take=200&filter=all&sort=added' + + req = self.session.prepare_request(Request('GET', self.server.url + endpoint)) + get_req = connection_handler(self.session, req, self.server.verify_ssl) or [] + + if not any([get_req]): + self.logger.error('No json replies. Discarding job') + return + + tv_requests = [] + movie_requests = [] + + for result in get_req['results']: + if result['type'] == 'tv': + try: + tv_requests.append(OverseerrRequest(**result)) + except TypeError as e: + self.logger.error('TypeError has occurred : %s while creating OverseerrRequest structure for show. ' + 'data attempted is: %s', e, result) + + if result['type'] == 'movie': + try: + movie_requests.append(OverseerrRequest(**result)) + except TypeError as e: + self.logger.error('TypeError has occurred : %s while creating OverseerrRequest \ + structure for movie. ' + 'data attempted is: %s', e, result) + + if tv_requests: + tv_request_count = len(tv_requests) + + if movie_requests: + movie_request_count = len(movie_requests) + + influx_payload = [ + { + "measurement": "Overseerr", + "tags": { + "type": "Request_Totals", + "server": self.server.id + }, + "time": now, + "fields": { + "total": movie_request_count + tv_request_count, + "movies": movie_request_count, + "tv": tv_request_count + } + } + ] + + if influx_payload: + self.dbmanager.write_points(influx_payload) + else: + self.logger.debug("Empty dataset for overseerr module. Discarding...") + + def get_request_status_counts(self): + now = datetime.now(timezone.utc).astimezone().isoformat() + endpoint = '/api/v1/request/count' + + req = self.session.prepare_request(Request('GET', self.server.url + endpoint)) + get_req = connection_handler(self.session, req, self.server.verify_ssl) + + if not get_req: + return + + requests = OverseerrRequestCounts(**get_req) + influx_payload = [ + { + "measurement": "Overseerr", + "tags": { + "type": "Request_Counts" + }, + "time": now, + "fields": { + "pending": requests.pending, + "approved": requests.approved, + "processing": requests.processing, + "available": requests.available + } + } + ] + + self.dbmanager.write_points(influx_payload) + + def get_latest_requests(self): + now = datetime.now(timezone.utc).astimezone().isoformat() + endpoint = '/api/v1/request?take=' + str(self.server.num_latest_requests_to_fetch) + '&filter=all&sort=added' + movie_endpoint = '/api/v1/movie/' + tv_endpoint = '/api/v1/tv/' + + # GET THE LATEST n REQUESTS + req = self.session.prepare_request(Request('GET', self.server.url + endpoint)) + get_latest_req = connection_handler(self.session, req, self.server.verify_ssl) + + # RETURN NOTHING IF NO RESULTS + if not get_latest_req: + return + + influx_payload = [] + + # Request Type: Movie = 1, TV Show = 0 + for result in get_latest_req['results']: + if result['type'] == 'tv': + req = self.session.prepare_request(Request('GET', + self.server.url + + tv_endpoint + + str(result['media']['tmdbId']))) + get_tv_req = connection_handler(self.session, req, self.server.verify_ssl) + hash_id = hashit(f'{get_tv_req["id"]}{get_tv_req["name"]}') + + influx_payload.append( + { + "measurement": "Overseerr", + "tags": { + "type": "Requests", + "server": self.server.id, + "request_type": 0, + "status": get_tv_req['mediaInfo']['status'], + "title": get_tv_req['name'], + "requested_user": get_tv_req['mediaInfo']['requests'][0]['requestedBy']['plexUsername'], + "requested_date": get_tv_req['mediaInfo']['requests'][0]['requestedBy']['createdAt'] + }, + "time": now, + "fields": { + "hash": hash_id + } + } + ) + + if result['type'] == 'movie': + req = self.session.prepare_request(Request('GET', + self.server.url + + movie_endpoint + + str(result['media']['tmdbId']))) + get_movie_req = connection_handler(self.session, req, self.server.verify_ssl) + hash_id = hashit(f'{get_movie_req["id"]}{get_movie_req["title"]}') + + influx_payload.append( + { + "measurement": "Overseerr", + "tags": { + "type": "Requests", + "server": self.server.id, + "request_type": 1, + "status": get_movie_req['mediaInfo']['status'], + "title": get_movie_req['title'], + "requested_user": get_movie_req['mediaInfo']['requests'][0]['requestedBy']['plexUsername'], + "requested_date": get_movie_req['mediaInfo']['requests'][0]['requestedBy']['createdAt'] + }, + "time": now, + "fields": { + "hash": hash_id + } + } + ) + + self.dbmanager.write_points(influx_payload) diff --git a/varken/structures.py b/varken/structures.py index f799ab4..950c3d3 100644 --- a/varken/structures.py +++ b/varken/structures.py @@ -57,6 +57,20 @@ class OmbiServer(NamedTuple): verify_ssl: bool = False +class OverseerrServer(NamedTuple): + api_key: str = None + id: int = None + url: str = None + verify_ssl: bool = False + get_request_total_counts: bool = False + request_total_run_seconds: int = 30 + get_request_status_counts: bool = False + request_status_run_seconds: int = 30 + get_latest_requests: bool = False + num_latest_requests_to_fetch: int = 10 + num_latest_requests_seconds: int = 30 + + class TautulliServer(NamedTuple): api_key: str = None fallback_ip: str = None @@ -173,6 +187,60 @@ class OmbiMovieRequest(NamedTuple): requestStatus: str = None +# Overseerr +class OverseerrRequest(NamedTuple): + id: int = None + status: int = None + createdAt: str = None + updatedAt: str = None + type: str = None + is4k: bool = None + serverId: int = None + profileId: int = None + rootFolder: str = None + languageProfileId: int = None + tags: list = None + media: dict = None + seasons: list = None + modifiedBy: dict = None + requestedBy: dict = None + seasonCount: int = None + + +class OverseerrRequestCounts(NamedTuple): + pending: int = None + approved: int = None + processing: int = None + available: int = None + + +# Overseerr +class OverseerrRequest(NamedTuple): + id: int = None + status: int = None + createdAt: str = None + updatedAt: str = None + type: str = None + is4k: bool = None + serverId: int = None + profileId: int = None + rootFolder: str = None + languageProfileId: int = None + tags: list = None + media: dict = None + seasons: list = None + modifiedBy: dict = None + requestedBy: dict = None + seasonCount: int = None + + +class OverseerrRequestCounts(NamedTuple): + pending: int = None + approved: int = None + processing: int = None + available: int = None + + # Sonarr class SonarrTVShow(NamedTuple): added: str = None