Migrated tautulli.py and allowed for multiple servers

This commit is contained in:
Nicholas St. Germain 2018-12-01 16:30:41 -06:00
parent 32b965edf8
commit 7638cd937e
8 changed files with 439 additions and 206 deletions

2
.gitignore vendored
View file

@ -12,4 +12,4 @@ GeoLite2-City.mmdb
GeoLite2-City.tar.gz
.idea/
.idea/*
Varken/varken.ini
varken.ini

View file

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

View file

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

View file

@ -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'):

View file

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

146
Varken/tautulli.py Normal file
View file

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

View file

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

View file

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