diff --git a/CHANGELOG.md b/CHANGELOG.md index eda655d..6ad9ec5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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:** diff --git a/README.md b/README.md index fe68aa7..544f703 100644 --- a/README.md +++ b/README.md @@ -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/)

Example Dashboard - +

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! \ No newline at end of file diff --git a/Varken.py b/Varken.py index 20aea2b..c157646 100644 --- a/Varken.py +++ b/Varken.py @@ -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: diff --git a/data/varken.example.ini b/data/varken.example.ini index b061984..bbb2ceb 100644 --- a/data/varken.example.ini +++ b/data/varken.example.ini @@ -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 diff --git a/varken/__init__.py b/varken/__init__.py index e19b0bd..fba0206 100644 --- a/varken/__init__.py +++ b/varken/__init__.py @@ -1,2 +1,2 @@ -VERSION = 1.4 +VERSION = 1.5 BRANCH = 'master' diff --git a/varken/helpers.py b/varken/helpers.py index fd13385..648eb5a 100644 --- a/varken/helpers.py +++ b/varken/helpers.py @@ -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' diff --git a/varken/iniparser.py b/varken/iniparser.py index dcd2e44..3569503 100644 --- a/varken/iniparser.py +++ b/varken/iniparser.py @@ -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) diff --git a/varken/ombi.py b/varken/ombi.py index f9a4830..86f8e33 100644 --- a/varken/ombi.py +++ b/varken/ombi.py @@ -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) diff --git a/varken/radarr.py b/varken/radarr.py index d5ad514..e8a15d0 100644 --- a/varken/radarr.py +++ b/varken/radarr.py @@ -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: diff --git a/varken/sonarr.py b/varken/sonarr.py index 19a36c9..b90dad9 100644 --- a/varken/sonarr.py +++ b/varken/sonarr.py @@ -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) diff --git a/varken/structures.py b/varken/structures.py index accd4fe..0672f50 100644 --- a/varken/structures.py +++ b/varken/structures.py @@ -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 diff --git a/varken/tautulli.py b/varken/tautulli.py index 5bde35a..336bba9 100644 --- a/varken/tautulli.py +++ b/varken/tautulli.py @@ -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(),