diff --git a/.gitignore b/.gitignore index c51a719..185ff03 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,4 @@ GeoLite2-City.mmdb GeoLite2-City.tar.gz .idea/ .idea/* -Varken/varken.ini +varken.ini diff --git a/Legacy/tautulli.py b/Legacy/tautulli.py deleted file mode 100644 index 70656df..0000000 --- a/Legacy/tautulli.py +++ /dev/null @@ -1,179 +0,0 @@ -import os -import tarfile -import urllib.request -import time -from datetime import datetime, timezone -import geoip2.database -from influxdb import InfluxDBClient -import requests -from Varken import configuration - -CURRENT_TIME = datetime.now(timezone.utc).astimezone().isoformat() - -PAYLOAD = {'apikey': configuration.tautulli_api_key, 'cmd': 'get_activity'} - -ACTIVITY = requests.get('{}/api/v2'.format(configuration.tautulli_url), - params=PAYLOAD).json()['response']['data'] - -SESSIONS = {d['session_id']: d for d in ACTIVITY['sessions']} - -TAR_DBFILE = '{}/GeoLite2-City.tar.gz'.format(os.path.dirname(os.path.realpath(__file__))) - -DBFILE = '{}/GeoLite2-City.mmdb'.format(os.path.dirname(os.path.realpath(__file__))) - -NOW = time.time() - -DB_AGE = NOW - (86400 * 35) - -#remove the running db file if it is older than 35 days -try: - t = os.stat(DBFILE) - c = t.st_ctime - if c < DB_AGE: - os.remove(DBFILE) -except FileNotFoundError: - pass - - -def geo_lookup(ipaddress): - """Lookup an IP using the local GeoLite2 DB""" - if not os.path.isfile(DBFILE): - urllib.request.urlretrieve( - 'http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.tar.gz', - TAR_DBFILE) - - tar = tarfile.open(TAR_DBFILE, "r:gz") - for files in tar.getmembers(): - if 'GeoLite2-City.mmdb' in files.name: - files.name = os.path.basename(files.name) - tar.extract(files, '{}/'.format(os.path.dirname(os.path.realpath(__file__)))) - - reader = geoip2.database.Reader(DBFILE) - - return reader.city(ipaddress) - - -INFLUX_PAYLOAD = [ - { - "measurement": "Tautulli", - "tags": { - "type": "stream_count" - }, - "time": CURRENT_TIME, - "fields": { - "current_streams": int(ACTIVITY['stream_count']), - "transcode_streams": int(ACTIVITY['stream_count_transcode']), - "direct_play_streams": int(ACTIVITY['stream_count_direct_play']), - "direct_streams": int(ACTIVITY['stream_count_direct_stream']) - } - } -] - -for session in SESSIONS.keys(): - try: - geodata = geo_lookup(SESSIONS[session]['ip_address_public']) - except (ValueError, geoip2.errors.AddressNotFoundError): - if configuration.tautulli_failback_ip: - geodata = geo_lookup(configuration.tautulli_failback_ip) - else: - geodata = geo_lookup(requests.get('http://ip.42.pl/raw').text) - - latitude = geodata.location.latitude - - if not geodata.location.latitude: - latitude = 37.234332396 - else: - latitude = geodata.location.latitude - - if not geodata.location.longitude: - longitude = -115.80666344 - else: - longitude = geodata.location.longitude - - decision = SESSIONS[session]['transcode_decision'] - - if decision == 'copy': - decision = 'direct stream' - - video_decision = SESSIONS[session]['stream_video_decision'] - - if video_decision == 'copy': - video_decision = 'direct stream' - - elif video_decision == '': - video_decision = 'Music' - - quality = SESSIONS[session]['stream_video_resolution'] - - - # If the video resolution is empty. Asssume it's an audio stream - # and use the container for music - if not quality: - quality = SESSIONS[session]['container'].upper() - - elif quality in ('SD', 'sd'): - quality = SESSIONS[session]['stream_video_resolution'].upper() - - elif quality in '4k': - quality = SESSIONS[session]['stream_video_resolution'].upper() - - else: - quality = '{}p'.format(SESSIONS[session]['stream_video_resolution']) - - - # Translate player_state to integers so we can colorize the table - player_state = SESSIONS[session]['state'].lower() - - if player_state == 'playing': - player_state = 0 - - elif player_state == 'paused': - player_state = 1 - - elif player_state == 'buffering': - player_state = 3 - - - INFLUX_PAYLOAD.append( - { - "measurement": "Tautulli", - "tags": { - "type": "Session", - "session_id": SESSIONS[session]['session_id'], - "name": SESSIONS[session]['friendly_name'], - "title": SESSIONS[session]['full_title'], - "platform": SESSIONS[session]['platform'], - "product_version": SESSIONS[session]['product_version'], - "quality": quality, - "video_decision": video_decision.title(), - "transcode_decision": decision.title(), - "media_type": SESSIONS[session]['media_type'].title(), - "audio_codec": SESSIONS[session]['audio_codec'].upper(), - "audio_profile": SESSIONS[session]['audio_profile'].upper(), - "stream_audio_codec": SESSIONS[session]['stream_audio_codec'].upper(), - "quality_profile": SESSIONS[session]['quality_profile'], - "progress_percent": SESSIONS[session]['progress_percent'], - "region_code": geodata.subdivisions.most_specific.iso_code, - "location": geodata.city.name, - "full_location": '{} - {}'.format(geodata.subdivisions.most_specific.name, - geodata.city.name), - "latitude": latitude, - "longitude": longitude, - "player_state": player_state, - "device_type": SESSIONS[session]['platform'] - }, - "time": CURRENT_TIME, - "fields": { - "session_id": SESSIONS[session]['session_id'], - "session_key": SESSIONS[session]['session_key'] - } - } - ) - -INFLUX_SENDER = InfluxDBClient(configuration.influxdb_url, - configuration.influxdb_port, - configuration.influxdb_username, - configuration.influxdb_password, - configuration.tautulli_influxdb_db_name) - -INFLUX_SENDER.write_points(INFLUX_PAYLOAD) diff --git a/Varken/helpers.py b/Varken/helpers.py index 2551928..fea68b1 100644 --- a/Varken/helpers.py +++ b/Varken/helpers.py @@ -1,4 +1,10 @@ +import os +import time +import tarfile +import geoip2.database from typing import NamedTuple +from os.path import abspath, join +from urllib.request import urlretrieve class TVShow(NamedTuple): @@ -35,17 +41,19 @@ class Queue(NamedTuple): protocol: str = None id: int = None + class SonarrServer(NamedTuple): id: int = None url: str = None api_key: str = None verify_ssl: bool = False missing_days: int = 0 - missing_days_run_minutes: int = 30 + missing_days_run_seconds: int = 30 future_days: int = 0 - future_days_run_minutes: int = 30 + future_days_run_seconds: int = 30 queue: bool = False - queue_run_minutes: int = 1 + queue_run_seconds: int = 1 + class Server(NamedTuple): id: int = None @@ -55,14 +63,238 @@ class Server(NamedTuple): class TautulliServer(NamedTuple): + id: int = None url: str = None fallback_ip: str = None apikey: str = None verify_ssl: bool = None - influx_db: str = None + get_activity: bool = False + get_activity_run_seconds: int = 30 + get_sessions: bool = False + get_sessions_run_seconds: int = 30 + class InfluxServer(NamedTuple): url: str = 'localhost' port: int = 8086 username: str = 'root' - password: str = 'root' \ No newline at end of file + password: str = 'root' + + +class TautulliStream(NamedTuple): + rating: str + transcode_width: str + labels: list + stream_bitrate: str + bandwidth: str + optimized_version: int + video_language: str + parent_rating_key: str + rating_key: str + platform_version: str + transcode_hw_decoding: int + thumb: str + title: str + video_codec_level: str + tagline: str + last_viewed_at: str + audio_sample_rate: str + user_rating: str + platform: str + collections: list + location: str + transcode_container: str + audio_channel_layout: str + local: str + stream_subtitle_format: str + stream_video_ref_frames: str + transcode_hw_encode_title: str + stream_container_decision: str + audience_rating: str + full_title: str + ip_address: str + subtitles: int + stream_subtitle_language: str + channel_stream: int + video_bitrate: str + is_allow_sync: int + stream_video_bitrate: str + summary: str + stream_audio_decision: str + aspect_ratio: str + audio_bitrate_mode: str + transcode_hw_decode_title: str + stream_audio_channel_layout: str + deleted_user: int + library_name: str + art: str + stream_video_resolution: str + video_profile: str + sort_title: str + stream_video_codec_level: str + stream_video_height: str + year: str + stream_duration: str + stream_audio_channels: str + video_language_code: str + transcode_key: str + transcode_throttled: int + container: str + stream_audio_bitrate: str + user: str + selected: int + product_version: str + subtitle_location: str + transcode_hw_requested: int + video_height: str + state: str + is_restricted: int + email: str + stream_container: str + transcode_speed: str + video_bit_depth: str + stream_audio_sample_rate: str + grandparent_title: str + studio: str + transcode_decision: str + video_width: str + bitrate: str + machine_id: str + originally_available_at: str + video_frame_rate: str + synced_version_profile: str + friendly_name: str + audio_profile: str + optimized_version_title: str + platform_name: str + stream_video_language: str + keep_history: int + stream_audio_codec: str + stream_video_codec: str + grandparent_thumb: str + synced_version: int + transcode_hw_decode: str + user_thumb: str + stream_video_width: str + height: str + stream_subtitle_decision: str + audio_codec: str + parent_title: str + guid: str + audio_language_code: str + transcode_video_codec: str + transcode_audio_codec: str + stream_video_decision: str + user_id: int + transcode_height: str + transcode_hw_full_pipeline: int + throttled: str + quality_profile: str + width: str + live: int + stream_subtitle_forced: int + media_type: str + video_resolution: str + stream_subtitle_location: str + do_notify: int + video_ref_frames: str + stream_subtitle_language_code: str + audio_channels: str + stream_audio_language_code: str + optimized_version_profile: str + relay: int + duration: str + rating_image: str + is_home_user: int + is_admin: int + ip_address_public: str + allow_guest: int + transcode_audio_channels: str + stream_audio_channel_layout_: str + media_index: str + stream_video_framerate: str + transcode_hw_encode: str + grandparent_rating_key: str + original_title: str + added_at: str + banner: str + bif_thumb: str + parent_media_index: str + live_uuid: str + audio_language: str + stream_audio_bitrate_mode: str + username: str + subtitle_decision: str + children_count: str + updated_at: str + player: str + subtitle_format: str + file: str + file_size: str + session_key: str + id: str + subtitle_container: str + genres: list + stream_video_language_code: str + indexes: int + video_decision: str + stream_audio_language: str + writers: list + actors: list + progress_percent: str + audio_decision: str + subtitle_forced: int + profile: str + product: str + view_offset: str + type: str + audience_rating_image: str + audio_bitrate: str + section_id: str + stream_subtitle_codec: str + subtitle_codec: str + video_codec: str + device: str + stream_video_bit_depth: str + video_framerate: str + transcode_hw_encoding: int + transcode_protocol: str + shared_libraries: list + stream_aspect_ratio: str + content_rating: str + session_id: str + directors: list + parent_thumb: str + subtitle_language_code: str + transcode_progress: int + subtitle_language: str + stream_subtitle_container: str + +def geoip_download(): + tar_dbfile = abspath(join('..', 'data', 'GeoLite2-City.tar.gz')) + url = 'http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.tar.gz' + urlretrieve(url, tar_dbfile) + tar = tarfile.open(tar_dbfile, "r:gz") + for files in tar.getmembers(): + if 'GeoLite2-City.mmdb' in files.name: + files.name = os.path.basename(files.name) + tar.extract(files, '{}/'.format(os.path.dirname(os.path.realpath(__file__)))) + os.remove(tar_dbfile) + +def geo_lookup(ipaddress): + + dbfile = abspath(join('..', 'data', 'GeoLite2-City.mmdb')) + now = time.time() + + try: + dbinfo = os.stat(dbfile) + db_age = now - dbinfo.st_ctime + if db_age > (35 * 86400): + os.remove(dbfile) + geoip_download() + except FileNotFoundError: + geoip_download() + + reader = geoip2.database.Reader(dbfile) + + return reader.city(ipaddress) diff --git a/Varken/iniparser.py b/Varken/iniparser.py index 6c6875f..449d052 100644 --- a/Varken/iniparser.py +++ b/Varken/iniparser.py @@ -20,7 +20,7 @@ class INIParser(object): self.ombi_server = None self.tautulli_enabled = False - self.tautulli_server = None + self.tautulli_servers = [] self.asa_enabled = False self.asa = None @@ -67,9 +67,10 @@ class INIParser(object): future_days_run_seconds = self.config.getint(sonarr_section, 'future_days_run_seconds') queue_run_seconds = self.config.getint(sonarr_section, 'queue_run_seconds') - self.sonarr_servers.append(SonarrServer(server_id, scheme + url, apikey, verify_ssl, missing_days, - missing_days_run_seconds, future_days, - future_days_run_seconds, queue, queue_run_seconds)) + server = SonarrServer(server_id, scheme + url, apikey, verify_ssl, missing_days, + missing_days_run_seconds, future_days, future_days_run_seconds, + queue, queue_run_seconds) + self.sonarr_servers.append(server) # Parse Radarr options try: @@ -79,6 +80,8 @@ class INIParser(object): self.radarr_enabled = True except ValueError: self.radarr_enabled = True + + if self.sonarr_enabled: sids = self.config.get('global', 'radarr_server_ids').strip(' ').split(',') for server_id in sids: @@ -91,16 +94,32 @@ class INIParser(object): self.radarr_servers.append(Server(server_id, scheme + url, apikey, verify_ssl)) # Parse Tautulli options - if self.config.getboolean('global', 'tautulli'): + try: + if not self.config.getboolean('global', 'tautulli_server_ids'): + sys.exit('tautulli_server_ids must be either false, or a comma-separated list of server ids') + elif self.config.getint('global', 'tautulli_server_ids'): + self.tautulli_enabled = True + except ValueError: self.tautulli_enabled = True - url = self.config.get('tautulli', 'url') - fallback_ip = self.config.get('tautulli', 'fallback_ip') - apikey = self.config.get('tautulli', 'apikey') - scheme = 'https://' if self.config.getboolean('tautulli', 'ssl') else 'http://' - verify_ssl = self.config.getboolean('tautulli', 'verify_ssl') - db_name = self.config.get('tautulli', 'influx_db') - self.tautulli_server = TautulliServer(scheme + url, fallback_ip, apikey, verify_ssl, db_name) + if self.tautulli_enabled: + sids = self.config.get('global', 'tautulli_server_ids').strip(' ').split(',') + + for server_id in sids: + tautulli_section = 'tautulli-' + server_id + url = self.config.get(tautulli_section, 'url') + fallback_ip = self.config.get(tautulli_section, 'fallback_ip') + apikey = self.config.get(tautulli_section, 'apikey') + scheme = 'https://' if self.config.getboolean(tautulli_section, 'ssl') else 'http://' + verify_ssl = self.config.getboolean(tautulli_section, 'verify_ssl') + get_activity = self.config.getboolean(tautulli_section, 'get_activity') + get_activity_run_seconds = self.config.getint(tautulli_section, 'get_activity_run_seconds') + get_sessions = self.config.getboolean(tautulli_section, 'get_sessions') + get_sessions_run_seconds = self.config.getint(tautulli_section, 'get_sessions_run_seconds') + + server = TautulliServer(server_id, scheme + url, fallback_ip, apikey, verify_ssl, get_activity, + get_activity_run_seconds, get_sessions, get_sessions_run_seconds) + self.tautulli_servers.append(server) # Parse Ombi Options if self.config.getboolean('global', 'ombi'): diff --git a/Varken/sonarr.py b/Varken/sonarr.py index c93a067..65f5df0 100644 --- a/Varken/sonarr.py +++ b/Varken/sonarr.py @@ -9,13 +9,13 @@ from Varken.helpers import TVShow, Queue class SonarrAPI(object): - def __init__(self, sonarr_servers, influx_server): + def __init__(self, servers, influx_server): # Set Time of initialization self.now = datetime.now(timezone.utc).astimezone().isoformat() self.today = str(date.today()) self.influx = InfluxDBClient(influx_server.url, influx_server.port, influx_server.username, influx_server.password, 'plex') - self.servers = sonarr_servers + self.servers = servers # Create session to reduce server web thread load, and globally define pageSize for all requests self.session = requests.Session() self.session.params = {'pageSize': 1000} diff --git a/Varken/tautulli.py b/Varken/tautulli.py new file mode 100644 index 0000000..6912c90 --- /dev/null +++ b/Varken/tautulli.py @@ -0,0 +1,146 @@ +from datetime import datetime, timezone +from geoip2.errors import AddressNotFoundError +from influxdb import InfluxDBClient +import requests +from Varken.helpers import TautulliStream, geo_lookup +from Varken.logger import logging + +class TautulliAPI(object): + def __init__(self, servers, influx_server): + # Set Time of initialization + self.now = datetime.now(timezone.utc).astimezone().isoformat() + self.influx = InfluxDBClient(influx_server.url, influx_server.port, influx_server.username, + influx_server.password, 'plex') + self.servers = servers + self.session = requests.Session() + self.endpoint = '/api/v2' + + def influx_push(self, payload): + # TODO: error handling for failed connection + self.influx.write_points(payload) + + @logging + def get_activity(self, notimplemented): + params = {'cmd': 'get_activity'} + influx_payload = [] + + for server in self.servers: + params['apikey'] = server.apikey + g = self.session.get(server.url + self.endpoint, params=params, verify=server.verify_ssl) + get = g.json()['response']['data'] + + influx_payload.append( + { + "measurement": "Tautulli", + "tags": { + "type": "current_stream_stats", + "server": server.id + }, + "time": self.now, + "fields": { + "stream_count": int(get['stream_count']), + "total_bandwidth": int(get['total_bandwidth']), + "wan_bandwidth": int(get['wan_bandwidth']), + "lan_bandwidth": int(get['lan_bandwidth']), + "transcode_streams": int(get['stream_count_transcode']), + "direct_play_streams": int(get['stream_count_direct_play']), + "direct_streams": int(get['stream_count_direct_stream']) + } + } + ) + + self.influx_push(influx_payload) + + @logging + def get_sessions(self, notimplemented): + params = {'cmd': 'get_activity'} + influx_payload = [] + + for server in self.servers: + params['apikey'] = server.apikey + g = self.session.get(server.url + self.endpoint, params=params, verify=server.verify_ssl) + get = g.json()['response']['data']['sessions'] + print(get) + sessions = [TautulliStream(**session) for session in get] + + for session in sessions: + try: + geodata = geo_lookup(session.ip_address_public) + except (ValueError, AddressNotFoundError): + if server.fallback_ip: + geodata = geo_lookup(server.fallback_ip) + else: + my_ip = requests.get('http://ip.42.pl/raw').text + geodata = geo_lookup(my_ip) + + if not all([geodata.location.latitude, geodata.location.longitude]): + latitude = 37.234332396 + longitude = -115.80666344 + else: + latitude = geodata.location.latitude + longitude = geodata.location.longitude + + decision = session.transcode_decision + if decision == 'copy': + decision = 'direct stream' + + video_decision = session.stream_video_decision + if video_decision == 'copy': + video_decision = 'direct stream' + elif video_decision == '': + video_decision = 'Music' + + quality = session.stream_video_resolution + if not quality: + quality = session.container.upper() + elif quality in ('SD', 'sd', '4k'): + quality = session.stream_video_resolution.upper() + else: + quality = session.stream_video_resolution + 'p' + + player_state = session.state.lower() + if player_state == 'playing': + player_state = 0 + elif player_state == 'paused': + player_state = 1 + elif player_state == 'buffering': + player_state = 3 + + influx_payload.append( + { + "measurement": "Tautulli", + "tags": { + "type": "Session", + "session_id": session.session_id, + "name": session.friendly_name, + "title": session.full_title, + "platform": session.platform, + "product_version": session.product_version, + "quality": quality, + "video_decision": video_decision.title(), + "transcode_decision": decision.title(), + "media_type": session.media_type.title(), + "audio_codec": session.audio_codec.upper(), + "audio_profile": session.audio_profile.upper(), + "stream_audio_codec": session.stream_audio_codec.upper(), + "quality_profile": session.quality_profile, + "progress_percent": session.progress_percent, + "region_code": geodata.subdivisions.most_specific.iso_code, + "location": geodata.city.name, + "full_location": '{} - {}'.format(geodata.subdivisions.most_specific.name, + geodata.city.name), + "latitude": latitude, + "longitude": longitude, + "player_state": player_state, + "device_type": session.platform, + "server": server.id + }, + "time": self.now, + "fields": { + "session_id": session.session_id, + "session_key": session.session_key + } + } + ) + + self.influx_push(influx_payload) diff --git a/Varken/varken.py b/Varken/varken.py index c6e9349..8be5b2c 100644 --- a/Varken/varken.py +++ b/Varken/varken.py @@ -4,6 +4,7 @@ from time import sleep from Varken.iniparser import INIParser from Varken.sonarr import SonarrAPI +from Varken.tautulli import TautulliAPI def threaded(job, days=None): @@ -27,6 +28,15 @@ if __name__ == "__main__": schedule.every(server.future_days_run_seconds).seconds.do(threaded, SONARR.get_future, server.future_days) + if CONFIG.tautulli_enabled: + TAUTULLI = TautulliAPI(CONFIG.tautulli_servers, CONFIG.influx_server) + + for server in CONFIG.tautulli_servers: + if server.get_activity: + schedule.every(server.get_activity_run_seconds).seconds.do(threaded, TAUTULLI.get_activity) + if server.get_sessions: + schedule.every(server.get_sessions_run_seconds).seconds.do(threaded, TAUTULLI.get_sessions) + while True: schedule.run_pending() sleep(1) diff --git a/varken.example.ini b/varken.example.ini index 10b7c70..aaa3bb6 100644 --- a/varken.example.ini +++ b/varken.example.ini @@ -8,8 +8,8 @@ [global] sonarr_server_ids = 1,2 radarr_server_ids = 1,2 +tautulli_server_ids = 1 ombi = true -tautulli = true asa = false [influxdb] @@ -18,6 +18,17 @@ port = 8086 username = root password = root +[tautulli-1] +url = tautulli.domain.tld +fallback_ip = 0.0.0.0 +apikey = xxxxxxxxxxxxxxxx +ssl = false +verify_ssl = true +get_activity = true +get_activity_run_seconds = 30 +get_sessions = true +get_sessions_run_seconds = 30 + [sonarr-1] url = sonarr1.domain.tld apikey = xxxxxxxxxxxxxxxx @@ -60,12 +71,6 @@ apikey = xxxxxxxxxxxxxxxx ssl = false verify_ssl = true -[tautulli] -url = tautulli.domain.tld -fallback_ip = 0.0.0.0 -apikey = xxxxxxxxxxxxxxxx -ssl = false -verify_ssl = true [asa] url = firewall.domain.tld