v1.5 Merge

This commit is contained in:
Nicholas St. Germain 2018-12-30 01:38:34 -06:00 committed by GitHub
commit d9637aca73
12 changed files with 524 additions and 414 deletions

View file

@ -1,6 +1,25 @@
# Change Log
## [v1.4](https://github.com/Boerderij/Varken/tree/v1.4) (2018-12-18)
## [v1.5](https://github.com/Boerderij/Varken/tree/v1.5) (2018-12-30)
[Full Changelog](https://github.com/Boerderij/Varken/compare/v1.4...v1.5)
**Implemented enhancements:**
- \[Feature Request\] Add issues from Ombi [\#70](https://github.com/Boerderij/Varken/issues/70)
- \[Feature Request\] Allow DNS Hostnames [\#66](https://github.com/Boerderij/Varken/issues/66)
- Replace static grafana configs with a Public Example [\#32](https://github.com/Boerderij/Varken/issues/32)
**Fixed bugs:**
- \[BUG\] unexpected keyword argument 'channel\_icon' [\#73](https://github.com/Boerderij/Varken/issues/73)
- \[BUG\] Unexpected keyword argument 'addOptions' [\#68](https://github.com/Boerderij/Varken/issues/68)
**Merged pull requests:**
- v1.5 Merge [\#75](https://github.com/Boerderij/Varken/pull/75) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- Add Ombi Issues [\#74](https://github.com/Boerderij/Varken/pull/74) ([anderssonoscar0](https://github.com/anderssonoscar0))
## [v1.4](https://github.com/Boerderij/Varken/tree/v1.4) (2018-12-19)
[Full Changelog](https://github.com/Boerderij/Varken/compare/v1.3-nightly...v1.4)
**Implemented enhancements:**
@ -92,10 +111,6 @@
## [v0.2-nightly](https://github.com/Boerderij/Varken/tree/v0.2-nightly) (2018-12-06)
[Full Changelog](https://github.com/Boerderij/Varken/compare/v0.1...v0.2-nightly)
**Implemented enhancements:**
- Tautulli - multiple server support? [\#25](https://github.com/Boerderij/Varken/issues/25)
**Closed issues:**
- Create the DB if it does not exist. [\#38](https://github.com/Boerderij/Varken/issues/38)
@ -105,7 +120,12 @@
- use a config.ini instead of command-line flags [\#33](https://github.com/Boerderij/Varken/issues/33)
- Migrate crontab to python schedule package [\#31](https://github.com/Boerderij/Varken/issues/31)
- Consolidate missing and missing\_days in sonarr.py [\#30](https://github.com/Boerderij/Varken/issues/30)
- Database Withou any scripts [\#29](https://github.com/Boerderij/Varken/issues/29)
- Grafana dashboard json doesn't match format of readme screenshot? [\#28](https://github.com/Boerderij/Varken/issues/28)
- Ombi something new \[Request\] [\#26](https://github.com/Boerderij/Varken/issues/26)
- Users Online not populating [\#24](https://github.com/Boerderij/Varken/issues/24)
- Missing dashboard [\#23](https://github.com/Boerderij/Varken/issues/23)
- Is there a Docker Image available for these scripts? [\#22](https://github.com/Boerderij/Varken/issues/22)
- Support for Linux without ASA [\#21](https://github.com/Boerderij/Varken/issues/21)
**Merged pull requests:**

View file

@ -10,14 +10,14 @@ from the Plex ecosystem into InfluxDB. Examples use Grafana for a
frontend
Requirements:
* Python3.6+
* Python3.6.7+
* Python3-pip
* [InfluxDB](https://www.influxdata.com/)
<p align="center">
Example Dashboard
<img width="800" src="https://nickflix.io/sharex/firefox_NxdrqisVLF.png">
<img width="800" src="https://i.imgur.com/G5bnpjs.png">
</p>
Supported Modules:
@ -103,9 +103,16 @@ do not include database creation, please ensure you create an influx database
named `varken`
### Grafana
[Grafana Installation Documentation](http://docs.grafana.org/installation/)
[Grafana Installation Documentation](http://docs.grafana.org/installation/)
[Official Example Dashboards](https://grafana.com/dashboards?search=Varken%20%5BOfficial%5D)
Grafana is used in our examples but not required, nor packaged as part of
Varken. Panel example pictures are pinned in the grafana-panels channel of
discord. Future releases may contain a json-generator, but it does not exist
as varken stands today.
Varken. Panel examples now exist in both nightly and tagged releases hosted
on grafana.com (link above).
1. Use the link above, then click on your desired dashboard version
2. Click `Copy ID to Clipboard`
3. In grafana, click your dashboards menu dropdown, and then click `Import dashboard`
4. Paste the ID into the `Grafana.com Dashboard` field and then click into empty space on the screen. (This should change the screen to show `Importing Dashboard from Grafana.com`
5. Select your varken datasource name in the dropdown labeled `Varken`
6. Click Import!

View file

@ -121,6 +121,8 @@ if __name__ == "__main__":
schedule.every(server.request_type_run_seconds).seconds.do(threaded, OMBI.get_request_counts)
if server.request_total_counts:
schedule.every(server.request_total_run_seconds).seconds.do(threaded, OMBI.get_all_requests)
if server.issue_status_counts:
schedule.every(server.issue_status_run_seconds).seconds.do(threaded, OMBI.get_issue_counts)
if CONFIG.sickchill_enabled:
for server in CONFIG.sickchill_servers:

View file

@ -2,26 +2,28 @@
# - Sonarr + Radarr scripts support multiple servers. You can remove the second
# server by putting a # in front of the lines and section name, and removing
# that number from your server_ids list
# - fallback_ip, This is used when there is no IP listed in tautulli.
# This can happen when you are streaming locally. This is usually your public IP.
# - fallback_ip, This is used when there is no IP listed in Tautulli.
# This can happen when you are streaming locally. Set this to your public IP.
# You do not need to change this value if your IP changes. This is only for
# location lookups when there is a failure.
[global]
sonarr_server_ids = 1,2
radarr_server_ids = 1,2
tautulli_server_ids = 1
ombi_server_ids = 1
ciscoasa_firewall_ids = false
ciscoasa_server_ids = false
sickchill_server_ids = false
[influxdb]
url = influxdb.domain.tld
port = 8086
username =
password =
username = root
password = root
[tautulli-1]
url = tautulli.domain.tld:8181
fallback_ip = 0.0.0.0
fallback_ip = 1.1.1.1
apikey = xxxxxxxxxxxxxxxx
ssl = false
verify_ssl = false
@ -83,6 +85,8 @@ get_request_type_counts = true
request_type_run_seconds = 300
get_request_total_counts = true
request_total_run_seconds = 300
get_issue_status_counts = true
issue_status_run_seconds = 300
[sickchill-1]
url = sickchill.domain.tld:8081
@ -92,8 +96,6 @@ verify_ssl = false
get_missing = true
get_missing_run_seconds = 300
[ciscoasa-1]
url = firewall.domain.tld
username = cisco

View file

@ -1,2 +1,2 @@
VERSION = 1.4
VERSION = 1.5
BRANCH = 'master'

View file

@ -27,17 +27,20 @@ class GeoIPHandler(object):
def lookup(self, ipaddress):
ip = ipaddress
self.logger.debug('Getting lat/long for Tautulli stream')
self.logger.debug('Getting lat/long for Tautulli stream using ip with last octet ending in %s',
ip.split('.')[-1:])
return self.reader.city(ip)
def update(self):
today = date.today()
dbdate = None
try:
dbdate = date.fromtimestamp(stat(self.dbfile).st_ctime)
except FileNotFoundError:
self.logger.error("Could not find GeoLite2 DB as: %s", self.dbfile)
self.download()
dbdate = date.fromtimestamp(stat(self.dbfile).st_ctime)
first_wednesday_day = [week[2:3][0] for week in monthcalendar(today.year, today.month) if week[2:3][0] != 0][0]
first_wednesday_date = date(today.year, today.month, first_wednesday_day)
@ -52,7 +55,6 @@ class GeoIPHandler(object):
else:
self.logger.debug('Geolite2 DB will update in %s days', abs(td.days))
def download(self):
tar_dbfile = abspath(join(self.data_folder, 'GeoLite2-City.tar.gz'))
url = 'http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.tar.gz'

View file

@ -59,15 +59,17 @@ class INIParser(object):
self.logger.error('Config file missing (varken.ini) in %s', self.data_folder)
exit(1)
def url_check(self, url=None, include_port=True):
def url_check(self, url=None, include_port=True, section=None):
url_check = url
module = section
inc_port = include_port
search = (r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain...
search = (r'(?:([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}|' # domain...
r'localhost|' # localhost...
r'^[a-zA-Z0-9_-]*|' # hostname only. My soul dies a little every time this is used...
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
)
# Include search for port if it is needed.
if inc_port:
search = (search + r'(?::\d+)?' + r'(?:/?|[/?]\S+)$')
else:
@ -78,28 +80,28 @@ class INIParser(object):
valid = match(regex, url_check) is not None
if not valid:
if inc_port:
self.logger.error('%s is invalid! URL must host/IP and port if not 80 or 443. ie. localhost:8080',
url_check)
self.logger.error('%s is invalid in module [%s]! URL must host/IP and '
'port if not 80 or 443. ie. localhost:8080',
url_check, module)
exit(1)
else:
self.logger.error('%s is invalid! URL must host/IP. ie. localhost', url_check)
self.logger.error('%s is invalid in module [%s]! URL must host/IP. ie. localhost', url_check, module)
exit(1)
else:
self.logger.debug('%s is a valid URL in the config.', url_check)
self.logger.debug('%s is a valid URL in module [%s].', url_check, module)
return url_check
def parse_opts(self):
self.read_file()
# Parse InfluxDB options
url = self.url_check(self.config.get('influxdb', 'url'), include_port=False)
url = self.url_check(self.config.get('influxdb', 'url'), include_port=False, section='influxdb')
port = self.config.getint('influxdb', 'port')
username = self.config.get('influxdb', 'username')
password = self.config.get('influxdb', 'password')
self.influx_server = InfluxServer(url, port, username, password)
self.influx_server = InfluxServer(url=url, port=port, username=username, password=password)
# Check for all enabled services
for service in self.services:
@ -111,7 +113,7 @@ class INIParser(object):
server = None
section = f"{service}-{server_id}"
try:
url = self.url_check(self.config.get(section, 'url'))
url = self.url_check(self.config.get(section, 'url'), section=section)
apikey = None
if service != 'ciscoasa':
@ -137,9 +139,11 @@ class INIParser(object):
queue_run_seconds = self.config.getint(section, '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)
server = SonarrServer(id=server_id, url=scheme + url, api_key=apikey, verify_ssl=verify_ssl,
missing_days=missing_days, future_days=future_days,
missing_days_run_seconds=missing_days_run_seconds,
future_days_run_seconds=future_days_run_seconds,
queue=queue, queue_run_seconds=queue_run_seconds)
if service == 'radarr':
queue = self.config.getboolean(section, 'queue')
@ -150,8 +154,9 @@ class INIParser(object):
get_missing_run_seconds = self.config.getint(section, 'get_missing_run_seconds')
server = RadarrServer(server_id, scheme + url, apikey, verify_ssl, queue, queue_run_seconds,
get_missing, get_missing_run_seconds)
server = RadarrServer(id=server_id, url=scheme + url, api_key=apikey, verify_ssl=verify_ssl,
queue_run_seconds=queue_run_seconds, get_missing=get_missing,
queue=queue, get_missing_run_seconds=get_missing_run_seconds)
if service == 'tautulli':
fallback_ip = self.config.get(section, 'fallback_ip')
@ -164,9 +169,11 @@ class INIParser(object):
get_stats_run_seconds = self.config.getint(section, 'get_stats_run_seconds')
server = TautulliServer(server_id, scheme + url, fallback_ip, apikey, verify_ssl,
get_activity, get_activity_run_seconds, get_stats,
get_stats_run_seconds)
server = TautulliServer(id=server_id, url=scheme + url, api_key=apikey,
verify_ssl=verify_ssl, get_activity=get_activity,
fallback_ip=fallback_ip, get_stats=get_stats,
get_activity_run_seconds=get_activity_run_seconds,
get_stats_run_seconds=get_stats_run_seconds)
if service == 'ombi':
request_type_counts = self.config.getboolean(section, 'get_request_type_counts')
@ -177,17 +184,26 @@ class INIParser(object):
request_total_run_seconds = self.config.getint(section, 'request_total_run_seconds')
server = OmbiServer(server_id, scheme + url, apikey, verify_ssl, request_type_counts,
request_type_run_seconds, request_total_counts,
request_total_run_seconds)
issue_status_counts = self.config.getboolean(section, 'get_issue_status_counts')
issue_status_run_seconds = self.config.getint(section, 'issue_status_run_seconds')
server = OmbiServer(id=server_id, url=scheme + url, api_key=apikey, verify_ssl=verify_ssl,
request_type_counts=request_type_counts,
request_type_run_seconds=request_type_run_seconds,
request_total_counts=request_total_counts,
request_total_run_seconds=request_total_run_seconds,
issue_status_counts=issue_status_counts,
issue_status_run_seconds=issue_status_run_seconds)
if service == 'sickchill':
get_missing = self.config.getboolean(section, 'get_missing')
get_missing_run_seconds = self.config.getint(section, 'get_missing_run_seconds')
server = SickChillServer(server_id, scheme + url, apikey, verify_ssl,
get_missing, get_missing_run_seconds)
server = SickChillServer(id=server_id, url=scheme + url, api_key=apikey,
verify_ssl=verify_ssl, get_missing=get_missing,
get_missing_run_seconds=get_missing_run_seconds)
if service == 'ciscoasa':
username = self.config.get(section, 'username')
@ -198,8 +214,10 @@ class INIParser(object):
get_bandwidth_run_seconds = self.config.getint(section, 'get_bandwidth_run_seconds')
server = CiscoASAFirewall(server_id, scheme + url, username, password, outside_interface,
verify_ssl, get_bandwidth_run_seconds)
server = CiscoASAFirewall(id=server_id, url=scheme + url, verify_ssl=verify_ssl,
username=username, password=password,
outside_interface=outside_interface,
get_bandwidth_run_seconds=get_bandwidth_run_seconds)
getattr(self, f'{service}_servers').append(server)

View file

@ -3,7 +3,7 @@ from requests import Session, Request
from datetime import datetime, timezone
from varken.helpers import connection_handler, hashit
from varken.structures import OmbiRequestCounts, OmbiMovieRequest, OmbiTVRequest
from varken.structures import OmbiRequestCounts, OmbiIssuesCounts, OmbiMovieRequest, OmbiTVRequest
class OmbiAPI(object):
@ -65,14 +65,17 @@ class OmbiAPI(object):
# Request Type: Movie = 1, TV Show = 0
for movie in movie_requests:
hash_id = hashit(f'{movie.id}{movie.theMovieDbId}{movie.title}')
status = None
# Denied = 0, Approved = 1, Completed = 2, Pending = 3
if movie.denied:
status = 0
elif movie.approved and movie.available:
status = 2
elif movie.approved:
status = 1
else:
status = 3
@ -101,10 +104,13 @@ class OmbiAPI(object):
# Denied = 0, Approved = 1, Completed = 2, Pending = 3
if show.childRequests[0]['denied']:
status = 0
elif show.childRequests[0]['approved'] and show.childRequests[0]['available']:
status = 2
elif show.childRequests[0]['approved']:
status = 1
else:
status = 3
@ -156,3 +162,31 @@ class OmbiAPI(object):
]
self.dbmanager.write_points(influx_payload)
def get_issue_counts(self):
now = datetime.now(timezone.utc).astimezone().isoformat()
endpoint = '/api/v1/Issues/count'
req = self.session.prepare_request(Request('GET', self.server.url + endpoint))
get = connection_handler(self.session, req, self.server.verify_ssl)
if not get:
return
requests = OmbiIssuesCounts(**get)
influx_payload = [
{
"measurement": "Ombi",
"tags": {
"type": "Issues_Counts"
},
"time": now,
"fields": {
"pending": requests.pending,
"in_progress": requests.inProgress,
"resolved": requests.resolved
}
}
]
self.dbmanager.write_points(influx_payload)

View file

@ -2,7 +2,7 @@ from logging import getLogger
from requests import Session, Request
from datetime import datetime, timezone
from varken.structures import Movie, Queue
from varken.structures import RadarrMovie, Queue
from varken.helpers import hashit, connection_handler
@ -31,9 +31,9 @@ class RadarrAPI(object):
return
try:
movies = [Movie(**movie) for movie in get]
movies = [RadarrMovie(**movie) for movie in get]
except TypeError as e:
self.logger.error('TypeError has occurred : %s while creating Movie structure', e)
self.logger.error('TypeError has occurred : %s while creating RadarrMovie structure', e)
return
for movie in movies:
@ -82,9 +82,9 @@ class RadarrAPI(object):
for movie in get:
try:
movie['movie'] = Movie(**movie['movie'])
movie['movie'] = RadarrMovie(**movie['movie'])
except TypeError as e:
self.logger.error('TypeError has occurred : %s while creating Movie structure', e)
self.logger.error('TypeError has occurred : %s while creating RadarrMovie structure', e)
return
try:

View file

@ -2,7 +2,7 @@ from logging import getLogger
from requests import Session, Request
from datetime import datetime, timezone, date, timedelta
from varken.structures import Queue, TVShow
from varken.structures import Queue, SonarrTVShow
from varken.helpers import hashit, connection_handler
@ -34,11 +34,11 @@ class SonarrAPI(object):
if not get:
return
# Iteratively create a list of TVShow Objects from response json
# Iteratively create a list of SonarrTVShow Objects from response json
try:
tv_shows = [TVShow(**show) for show in get]
tv_shows = [SonarrTVShow(**show) for show in get]
except TypeError as e:
self.logger.error('TypeError has occurred : %s while creating TVShow structure', e)
self.logger.error('TypeError has occurred : %s while creating SonarrTVShow structure', e)
return
# Add show to missing list if file does not exist
@ -87,9 +87,9 @@ class SonarrAPI(object):
return
try:
tv_shows = [TVShow(**show) for show in get]
tv_shows = [SonarrTVShow(**show) for show in get]
except TypeError as e:
self.logger.error('TypeError has occurred : %s while creating TVShow structure', e)
self.logger.error('TypeError has occurred : %s while creating SonarrTVShow structure', e)
return
for show in tv_shows:
@ -150,9 +150,9 @@ class SonarrAPI(object):
protocol_id = 0
queue.append((show.series['title'], show.episode['title'], show.protocol.upper(),
protocol_id, sxe, show.id))
protocol_id, sxe, show.id, show.quality['quality']['name']))
for series_title, episode_title, protocol, protocol_id, sxe, sonarr_id in queue:
for series_title, episode_title, protocol, protocol_id, sxe, sonarr_id, quality in queue:
hash_id = hashit(f'{self.server.id}{series_title}{sxe}')
influx_payload.append(
{
@ -165,7 +165,8 @@ class SonarrAPI(object):
"epname": episode_title,
"sxe": sxe,
"protocol": protocol,
"protocol_id": protocol_id
"protocol_id": protocol_id,
"quality": quality
},
"time": now,
"fields": {
@ -173,5 +174,4 @@ class SonarrAPI(object):
}
}
)
self.dbmanager.write_points(influx_payload)

View file

@ -4,418 +4,246 @@ from logging import getLogger
logger = getLogger('temp')
# Check for python3.6 or newer to resolve erroneous typing.NamedTuple issues
if version_info < (3, 6):
logger.error('Varken requires python3.6 or newer. You are on python%s.%s - Exiting...',
version_info.major, version_info.minor)
if version_info < (3, 6, 2):
logger.error('Varken requires python3.6.2 or newer. You are on python%s.%s.%s - Exiting...',
version_info.major, version_info.minor, version_info.micro)
exit(1)
class Queue(NamedTuple):
movie: dict = None
series: dict = None
episode: dict = None
quality: dict = None
size: float = None
title: str = None
sizeleft: float = None
timeleft: str = None
estimatedCompletionTime: str = None
status: str = None
trackedDownloadStatus: str = None
statusMessages: list = None
downloadId: str = None
protocol: str = None
id: int = None
# Server Structures
class InfluxServer(NamedTuple):
password: str = 'root'
port: int = 8086
url: str = 'localhost'
username: str = 'root'
class SonarrServer(NamedTuple):
id: int = None
url: str = None
api_key: str = None
verify_ssl: bool = False
missing_days: int = 0
missing_days_run_seconds: int = 30
future_days: int = 0
future_days_run_seconds: int = 30
id: int = None
missing_days: int = 0
missing_days_run_seconds: int = 30
queue: bool = False
queue_run_seconds: int = 30
url: str = None
verify_ssl: bool = False
class RadarrServer(NamedTuple):
id: int = None
url: str = None
api_key: str = None
verify_ssl: bool = False
queue: bool = False
queue_run_seconds: int = 30
get_missing: bool = False
get_missing_run_seconds: int = 30
id: int = None
queue: bool = False
queue_run_seconds: int = 30
url: str = None
verify_ssl: bool = False
class OmbiServer(NamedTuple):
id: int = None
url: str = None
api_key: str = None
verify_ssl: bool = False
request_type_counts: bool = False
request_type_run_seconds: int = 30
id: int = None
issue_status_counts: bool = False
issue_status_run_seconds: int = 30
request_total_counts: bool = False
request_total_run_seconds: int = 30
request_type_counts: bool = False
request_type_run_seconds: int = 30
url: str = None
verify_ssl: bool = False
class TautulliServer(NamedTuple):
id: int = None
url: str = None
fallback_ip: str = None
api_key: str = None
verify_ssl: bool = None
fallback_ip: str = None
get_activity: bool = False
get_activity_run_seconds: int = 30
get_stats: bool = False
get_stats_run_seconds: int = 30
class InfluxServer(NamedTuple):
url: str = 'localhost'
port: int = 8086
username: str = 'root'
password: str = 'root'
id: int = None
url: str = None
verify_ssl: bool = None
class SickChillServer(NamedTuple):
id: int = None
url: str = None
api_key: str = None
verify_ssl: bool = False
get_missing: bool = False
get_missing_run_seconds: int = 30
id: int = None
url: str = None
verify_ssl: bool = False
class CiscoASAFirewall(NamedTuple):
get_bandwidth_run_seconds: int = 30
id: int = None
outside_interface: str = None
password: str = 'cisco'
url: str = '192.168.1.1'
username: str = 'cisco'
password: str = 'cisco'
outside_interface: str = None
verify_ssl: bool = False
get_bandwidth_run_seconds: int = 30
# Shared
class Queue(NamedTuple):
downloadId: str = None
episode: dict = None
estimatedCompletionTime: str = None
id: int = None
movie: dict = None
protocol: str = None
quality: dict = None
series: dict = None
size: float = None
sizeleft: float = None
status: str = None
statusMessages: list = None
timeleft: str = None
title: str = None
trackedDownloadStatus: str = None
# Ombi Structures
class OmbiRequestCounts(NamedTuple):
pending: int = 0
approved: int = 0
available: int = 0
pending: int = 0
class TautulliStream(NamedTuple):
rating: str = None
transcode_width: str = None
labels: list = None
stream_bitrate: str = None
bandwidth: str = None
optimized_version: int = None
video_language: str = None
parent_rating_key: str = None
rating_key: str = None
platform_version: str = None
transcode_hw_decoding: int = None
thumb: str = None
title: str = None
video_codec_level: str = None
tagline: str = None
last_viewed_at: str = None
audio_sample_rate: str = None
user_rating: str = None
platform: str = None
collections: list = None
location: str = None
transcode_container: str = None
audio_channel_layout: str = None
local: str = None
stream_subtitle_format: str = None
stream_video_ref_frames: str = None
transcode_hw_encode_title: str = None
stream_container_decision: str = None
audience_rating: str = None
full_title: str = None
ip_address: str = None
subtitles: int = None
stream_subtitle_language: str = None
channel_stream: int = None
video_bitrate: str = None
is_allow_sync: int = None
stream_video_bitrate: str = None
summary: str = None
stream_audio_decision: str = None
aspect_ratio: str = None
audio_bitrate_mode: str = None
transcode_hw_decode_title: str = None
stream_audio_channel_layout: str = None
deleted_user: int = None
library_name: str = None
art: str = None
stream_video_resolution: str = None
video_profile: str = None
sort_title: str = None
stream_video_codec_level: str = None
stream_video_height: str = None
year: str = None
stream_duration: str = None
stream_audio_channels: str = None
video_language_code: str = None
transcode_key: str = None
transcode_throttled: int = None
container: str = None
stream_audio_bitrate: str = None
user: str = None
selected: int = None
product_version: str = None
subtitle_location: str = None
transcode_hw_requested: int = None
video_height: str = None
state: str = None
is_restricted: int = None
email: str = None
stream_container: str = None
transcode_speed: str = None
video_bit_depth: str = None
stream_audio_sample_rate: str = None
grandparent_title: str = None
studio: str = None
transcode_decision: str = None
video_width: str = None
bitrate: str = None
machine_id: str = None
originally_available_at: str = None
video_frame_rate: str = None
synced_version_profile: str = None
friendly_name: str = None
audio_profile: str = None
optimized_version_title: str = None
platform_name: str = None
stream_video_language: str = None
keep_history: int = None
stream_audio_codec: str = None
stream_video_codec: str = None
grandparent_thumb: str = None
synced_version: int = None
transcode_hw_decode: str = None
user_thumb: str = None
stream_video_width: str = None
height: str = None
stream_subtitle_decision: str = None
audio_codec: str = None
parent_title: str = None
guid: str = None
audio_language_code: str = None
transcode_video_codec: str = None
transcode_audio_codec: str = None
stream_video_decision: str = None
user_id: int = None
transcode_height: str = None
transcode_hw_full_pipeline: int = None
throttled: str = None
quality_profile: str = None
width: str = None
live: int = None
stream_subtitle_forced: int = None
media_type: str = None
video_resolution: str = None
stream_subtitle_location: str = None
do_notify: int = None
video_ref_frames: str = None
stream_subtitle_language_code: str = None
audio_channels: str = None
stream_audio_language_code: str = None
optimized_version_profile: str = None
relay: int = None
duration: str = None
rating_image: str = None
is_home_user: int = None
is_admin: int = None
ip_address_public: str = None
allow_guest: int = None
transcode_audio_channels: str = None
stream_audio_channel_layout_: str = None
media_index: str = None
stream_video_framerate: str = None
transcode_hw_encode: str = None
grandparent_rating_key: str = None
original_title: str = None
added_at: str = None
banner: str = None
bif_thumb: str = None
parent_media_index: str = None
live_uuid: str = None
audio_language: str = None
stream_audio_bitrate_mode: str = None
username: str = None
subtitle_decision: str = None
children_count: str = None
updated_at: str = None
player: str = None
subtitle_format: str = None
file: str = None
file_size: str = None
session_key: str = None
id: str = None
subtitle_container: str = None
genres: list = None
stream_video_language_code: str = None
indexes: int = None
video_decision: str = None
stream_audio_language: str = None
writers: list = None
actors: list = None
progress_percent: str = None
audio_decision: str = None
subtitle_forced: int = None
profile: str = None
product: str = None
view_offset: str = None
type: str = None
audience_rating_image: str = None
audio_bitrate: str = None
section_id: str = None
stream_subtitle_codec: str = None
subtitle_codec: str = None
video_codec: str = None
device: str = None
stream_video_bit_depth: str = None
video_framerate: str = None
transcode_hw_encoding: int = None
transcode_protocol: str = None
shared_libraries: list = None
stream_aspect_ratio: str = None
content_rating: str = None
session_id: str = None
directors: list = None
parent_thumb: str = None
subtitle_language_code: str = None
transcode_progress: int = None
subtitle_language: str = None
stream_subtitle_container: str = None
sub_type: str = None
extra_type: str = None
class TVShow(NamedTuple):
seriesId: int = None
episodeFileId: int = None
seasonNumber: int = None
episodeNumber: int = None
title: str = None
airDate: str = None
airDateUtc: str = None
overview: str = None
episodeFile: dict = None
hasFile: bool = None
monitored: bool = None
unverifiedSceneNumbering: bool = None
absoluteEpisodeNumber: int = None
sceneAbsoluteEpisodeNumber: int = None
sceneEpisodeNumber: int = None
sceneSeasonNumber: int = None
series: dict = None
id: int = None
class Movie(NamedTuple):
title: str = None
alternativeTitles: list = None
secondaryYearSourceId: int = None
sortTitle: str = None
sizeOnDisk: int = None
status: str = None
overview: str = None
inCinemas: str = None
images: list = None
downloaded: bool = None
year: int = None
secondaryYear: str = None
hasFile: bool = None
youTubeTrailerId: str = None
studio: str = None
path: str = None
profileId: int = None
pathState: str = None
monitored: bool = None
minimumAvailability: str = None
isAvailable: bool = None
folderName: str = None
runtime: int = None
lastInfoSync: str = None
cleanTitle: str = None
imdbId: str = None
tmdbId: int = None
titleSlug: str = None
genres: list = None
tags: list = None
added: str = None
ratings: dict = None
movieFile: dict = None
qualityProfileId: int = None
physicalRelease: str = None
physicalReleaseNote: str = None
website: str = None
id: int = None
class OmbiMovieRequest(NamedTuple):
theMovieDbId: int = None
issueId: None = None
issues: None = None
subscribed: bool = None
showSubscribe: bool = None
rootPathOverride: int = None
qualityOverride: int = None
imdbId: str = None
overview: str = None
posterPath: str = None
releaseDate: str = None
digitalReleaseDate: None = None
status: str = None
background: str = None
released: bool = None
digitalRelease: bool = None
title: str = None
approved: bool = None
markedAsApproved: str = None
requestedDate: str = None
available: bool = None
markedAsAvailable: None = None
requestedUserId: str = None
denied: bool = None
markedAsDenied: str = None
deniedReason: None = None
requestType: int = None
requestedUser: dict = None
canApprove: bool = None
id: int = None
class OmbiIssuesCounts(NamedTuple):
inProgress: int = 0
pending: int = 0
resolved: int = 0
class OmbiTVRequest(NamedTuple):
tvDbId: int = None
imdbId: str = None
qualityOverride: None = None
rootFolder: None = None
overview: str = None
title: str = None
posterPath: str = None
background: str = None
releaseDate: str = None
status: str = None
totalSeasons: int = None
childRequests: list = None
denied: bool = None
deniedReason: None = None
id: int = None
imdbId: str = None
markedAsDenied: str = None
overview: str = None
posterPath: str = None
qualityOverride: None = None
releaseDate: str = None
rootFolder: None = None
status: str = None
title: str = None
totalSeasons: int = None
tvDbId: int = None
class OmbiMovieRequest(NamedTuple):
approved: bool = None
available: bool = None
background: str = None
canApprove: bool = None
denied: bool = None
deniedReason: None = None
digitalRelease: bool = None
digitalReleaseDate: None = None
id: int = None
imdbId: str = None
issueId: None = None
issues: None = None
markedAsApproved: str = None
markedAsAvailable: None = None
markedAsDenied: str = None
overview: str = None
posterPath: str = None
qualityOverride: int = None
released: bool = None
releaseDate: str = None
requestedDate: str = None
requestedUser: dict = None
requestedUserId: str = None
requestType: int = None
rootPathOverride: int = None
showSubscribe: bool = None
status: str = None
subscribed: bool = None
theMovieDbId: int = None
title: str = None
# Sonarr
class SonarrTVShow(NamedTuple):
absoluteEpisodeNumber: int = None
airDate: str = None
airDateUtc: str = None
episodeFile: dict = None
episodeFileId: int = None
episodeNumber: int = None
hasFile: bool = None
id: int = None
lastSearchTime: str = None
monitored: bool = None
overview: str = None
sceneAbsoluteEpisodeNumber: int = None
sceneEpisodeNumber: int = None
sceneSeasonNumber: int = None
seasonNumber: int = None
series: dict = None
seriesId: int = None
title: str = None
unverifiedSceneNumbering: bool = None
# Radarr
class RadarrMovie(NamedTuple):
added: str = None
addOptions: str = None
alternativeTitles: list = None
certification: str = None
cleanTitle: str = None
downloaded: bool = None
folderName: str = None
genres: list = None
hasFile: bool = None
id: int = None
images: list = None
imdbId: str = None
inCinemas: str = None
isAvailable: bool = None
lastInfoSync: str = None
minimumAvailability: str = None
monitored: bool = None
movieFile: dict = None
overview: str = None
path: str = None
pathState: str = None
physicalRelease: str = None
physicalReleaseNote: str = None
profileId: int = None
qualityProfileId: int = None
ratings: dict = None
runtime: int = None
secondaryYear: str = None
secondaryYearSourceId: int = None
sizeOnDisk: int = None
sortTitle: str = None
status: str = None
studio: str = None
tags: list = None
title: str = None
titleSlug: str = None
tmdbId: int = None
website: str = None
year: int = None
youTubeTrailerId: str = None
# Sickchill
class SickChillTVShow(NamedTuple):
airdate: str = None
airs: str = None
episode: int = None
ep_name: str = None
ep_plot: str = None
episode: int = None
indexerid: int = None
network: str = None
paused: int = None
@ -425,3 +253,198 @@ class SickChillTVShow(NamedTuple):
show_status: str = None
tvdbid: int = None
weekday: int = None
# Tautulli
class TautulliStream(NamedTuple):
actors: list = None
added_at: str = None
allow_guest: int = None
art: str = None
aspect_ratio: str = None
audience_rating: str = None
audience_rating_image: str = None
audio_bitrate: str = None
audio_bitrate_mode: str = None
audio_channels: str = None
audio_channel_layout: str = None
audio_codec: str = None
audio_decision: str = None
audio_language: str = None
audio_language_code: str = None
audio_profile: str = None
audio_sample_rate: str = None
bandwidth: str = None
banner: str = None
bif_thumb: str = None
bitrate: str = None
channel_icon: str = None
channel_stream: int = None
channel_title: str = None
children_count: str = None
collections: list = None
container: str = None
content_rating: str = None
deleted_user: int = None
device: str = None
directors: list = None
do_notify: int = None
duration: str = None
email: str = None
extra_type: str = None
file: str = None
file_size: str = None
friendly_name: str = None
full_title: str = None
genres: list = None
grandparent_rating_key: str = None
grandparent_thumb: str = None
grandparent_title: str = None
guid: str = None
height: str = None
id: str = None
indexes: int = None
ip_address: str = None
ip_address_public: str = None
is_admin: int = None
is_allow_sync: int = None
is_home_user: int = None
is_restricted: int = None
keep_history: int = None
labels: list = None
last_viewed_at: str = None
library_name: str = None
live: int = None
live_uuid: str = None
local: str = None
location: str = None
machine_id: str = None
media_index: str = None
media_type: str = None
optimized_version: int = None
optimized_version_profile: str = None
optimized_version_title: str = None
originally_available_at: str = None
original_title: str = None
parent_media_index: str = None
parent_rating_key: str = None
parent_thumb: str = None
parent_title: str = None
platform: str = None
platform_name: str = None
platform_version: str = None
player: str = None
product: str = None
product_version: str = None
profile: str = None
progress_percent: str = None
quality_profile: str = None
rating: str = None
rating_image: str = None
rating_key: str = None
relay: int = None
section_id: str = None
selected: int = None
session_id: str = None
session_key: str = None
shared_libraries: list = None
sort_title: str = None
state: str = None
stream_aspect_ratio: str = None
stream_audio_bitrate: str = None
stream_audio_bitrate_mode: str = None
stream_audio_channels: str = None
stream_audio_channel_layout: str = None
stream_audio_channel_layout_: str = None
stream_audio_codec: str = None
stream_audio_decision: str = None
stream_audio_language: str = None
stream_audio_language_code: str = None
stream_audio_sample_rate: str = None
stream_bitrate: str = None
stream_container: str = None
stream_container_decision: str = None
stream_duration: str = None
stream_subtitle_codec: str = None
stream_subtitle_container: str = None
stream_subtitle_decision: str = None
stream_subtitle_forced: int = None
stream_subtitle_format: str = None
stream_subtitle_language: str = None
stream_subtitle_language_code: str = None
stream_subtitle_location: str = None
stream_video_bitrate: str = None
stream_video_bit_depth: str = None
stream_video_codec: str = None
stream_video_codec_level: str = None
stream_video_decision: str = None
stream_video_framerate: str = None
stream_video_height: str = None
stream_video_language: str = None
stream_video_language_code: str = None
stream_video_ref_frames: str = None
stream_video_resolution: str = None
stream_video_width: str = None
studio: str = None
subtitles: int = None
subtitle_codec: str = None
subtitle_container: str = None
subtitle_decision: str = None
subtitle_forced: int = None
subtitle_format: str = None
subtitle_language: str = None
subtitle_language_code: str = None
subtitle_location: str = None
sub_type: str = None
summary: str = None
synced_version: int = None
synced_version_profile: str = None
tagline: str = None
throttled: str = None
thumb: str = None
title: str = None
transcode_audio_channels: str = None
transcode_audio_codec: str = None
transcode_container: str = None
transcode_decision: str = None
transcode_height: str = None
transcode_hw_decode: str = None
transcode_hw_decode_title: str = None
transcode_hw_decoding: int = None
transcode_hw_encode: str = None
transcode_hw_encode_title: str = None
transcode_hw_encoding: int = None
transcode_hw_full_pipeline: int = None
transcode_hw_requested: int = None
transcode_key: str = None
transcode_progress: int = None
transcode_protocol: str = None
transcode_speed: str = None
transcode_throttled: int = None
transcode_video_codec: str = None
transcode_width: str = None
type: str = None
updated_at: str = None
user: str = None
username: str = None
user_id: int = None
user_rating: str = None
user_thumb: str = None
video_bitrate: str = None
video_bit_depth: str = None
video_codec: str = None
video_codec_level: str = None
video_decision: str = None
video_framerate: str = None
video_frame_rate: str = None
video_height: str = None
video_language: str = None
video_language_code: str = None
video_profile: str = None
video_ref_frames: str = None
video_resolution: str = None
video_width: str = None
view_offset: str = None
width: str = None
writers: list = None
year: str = None

View file

@ -108,6 +108,8 @@ class TautulliAPI(object):
"quality": quality,
"video_decision": video_decision.title(),
"transcode_decision": decision.title(),
"transcode_hw_decoding": session.transcode_hw_decoding,
"transcode_hw_encoding": session.transcode_hw_encoding,
"media_type": session.media_type.title(),
"audio_codec": session.audio_codec.upper(),
"audio_profile": session.audio_profile.upper(),