Rename; Removed deps from repo
This commit is contained in:
835
jellypy/api2.py
Normal file
835
jellypy/api2.py
Normal file
@@ -0,0 +1,835 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of Tautulli.
|
||||
#
|
||||
# Tautulli is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Tautulli is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from future.builtins import str
|
||||
from future.builtins import object
|
||||
|
||||
from hashing_passwords import check_hash
|
||||
from io import open
|
||||
|
||||
import hashlib
|
||||
import inspect
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
|
||||
import cherrypy
|
||||
import xmltodict
|
||||
|
||||
import jellypy
|
||||
if jellypy.PYTHON2:
|
||||
import common
|
||||
import config
|
||||
import database
|
||||
import helpers
|
||||
import libraries
|
||||
import logger
|
||||
import mobile_app
|
||||
import notification_handler
|
||||
import notifiers
|
||||
import newsletter_handler
|
||||
import newsletters
|
||||
import plextv
|
||||
import users
|
||||
else:
|
||||
from jellypy import common
|
||||
from jellypy import config
|
||||
from jellypy import database
|
||||
from jellypy import helpers
|
||||
from jellypy import libraries
|
||||
from jellypy import logger
|
||||
from jellypy import mobile_app
|
||||
from jellypy import notification_handler
|
||||
from jellypy import notifiers
|
||||
from jellypy import newsletter_handler
|
||||
from jellypy import newsletters
|
||||
from jellypy import plextv
|
||||
from jellypy import users
|
||||
|
||||
|
||||
class API2(object):
|
||||
def __init__(self, **kwargs):
|
||||
self._api_valid_methods = self._api_docs().keys()
|
||||
self._api_authenticated = False
|
||||
self._api_out_type = 'json' # default
|
||||
self._api_msg = None
|
||||
self._api_debug = None
|
||||
self._api_cmd = None
|
||||
self._api_apikey = None
|
||||
self._api_callback = None # JSONP
|
||||
self._api_result_type = 'error'
|
||||
self._api_response_code = None
|
||||
self._api_profileme = None # For profiling the api call
|
||||
self._api_kwargs = None # Cleaned kwargs
|
||||
self._api_app = False
|
||||
|
||||
def _api_docs(self, md=False):
|
||||
""" Makes the api docs. """
|
||||
|
||||
docs = {}
|
||||
for f, _ in inspect.getmembers(self, predicate=inspect.ismethod):
|
||||
if not f.startswith('_') and not f.startswith('_api'):
|
||||
if md is True:
|
||||
docs[f] = inspect.getdoc(getattr(self, f)) if inspect.getdoc(getattr(self, f)) else None
|
||||
else:
|
||||
docs[f] = ' '.join(inspect.getdoc(getattr(self, f)).split()) if inspect.getdoc(getattr(self, f)) else None
|
||||
return docs
|
||||
|
||||
def docs_md(self):
|
||||
""" Return the api docs formatted with markdown."""
|
||||
|
||||
return self._api_make_md()
|
||||
|
||||
def docs(self):
|
||||
""" Return the api docs as a dict where commands are keys, docstring are value."""
|
||||
|
||||
return self._api_docs()
|
||||
|
||||
def _api_validate(self, *args, **kwargs):
|
||||
""" Sets class vars and remove unneeded parameters. """
|
||||
|
||||
if not jellypy.CONFIG.API_ENABLED:
|
||||
self._api_msg = 'API not enabled'
|
||||
self._api_response_code = 404
|
||||
|
||||
elif not jellypy.CONFIG.API_KEY:
|
||||
self._api_msg = 'API key not generated'
|
||||
self._api_response_code = 401
|
||||
|
||||
elif len(jellypy.CONFIG.API_KEY) != 32:
|
||||
self._api_msg = 'API key not generated correctly'
|
||||
self._api_response_code = 401
|
||||
|
||||
elif 'apikey' not in kwargs:
|
||||
self._api_msg = 'Parameter apikey is required'
|
||||
self._api_response_code = 401
|
||||
|
||||
elif 'cmd' not in kwargs:
|
||||
self._api_msg = 'Parameter cmd is required. Possible commands are: %s' % ', '.join(self._api_valid_methods)
|
||||
self._api_response_code = 400
|
||||
|
||||
elif 'cmd' in kwargs and kwargs.get('cmd') not in self._api_valid_methods:
|
||||
self._api_msg = 'Unknown command: %s. Possible commands are: %s' % (kwargs.get('cmd', ''), ', '.join(sorted(self._api_valid_methods)))
|
||||
self._api_response_code = 400
|
||||
|
||||
self._api_callback = kwargs.pop('callback', None)
|
||||
self._api_apikey = kwargs.pop('apikey', None)
|
||||
self._api_cmd = kwargs.pop('cmd', None)
|
||||
self._api_debug = kwargs.pop('debug', False)
|
||||
self._api_profileme = kwargs.pop('profileme', None)
|
||||
# Allow override for the api.
|
||||
self._api_out_type = kwargs.pop('out_type', 'json')
|
||||
|
||||
if 'app' in kwargs and helpers.bool_true(kwargs.pop('app')):
|
||||
self._api_app = True
|
||||
|
||||
if jellypy.CONFIG.API_ENABLED and not self._api_msg or self._api_cmd in ('get_apikey', 'docs', 'docs_md'):
|
||||
if not self._api_app and self._api_apikey == jellypy.CONFIG.API_KEY:
|
||||
self._api_authenticated = True
|
||||
|
||||
elif self._api_app and self._api_apikey == mobile_app.get_temp_device_token() and \
|
||||
self._api_cmd == 'register_device':
|
||||
self._api_authenticated = True
|
||||
|
||||
elif self._api_app and mobile_app.get_mobile_device_by_token(self._api_apikey):
|
||||
mobile_app.set_last_seen(self._api_apikey)
|
||||
self._api_authenticated = True
|
||||
|
||||
else:
|
||||
self._api_msg = 'Invalid apikey'
|
||||
self._api_response_code = 401
|
||||
|
||||
if self._api_authenticated and self._api_cmd in self._api_valid_methods:
|
||||
self._api_msg = None
|
||||
self._api_kwargs = kwargs
|
||||
|
||||
elif not self._api_authenticated and self._api_cmd in ('get_apikey', 'docs', 'docs_md'):
|
||||
self._api_authenticated = True
|
||||
# Remove the old error msg
|
||||
self._api_msg = None
|
||||
self._api_kwargs = kwargs
|
||||
|
||||
if self._api_msg:
|
||||
logger.api_debug('Tautulli APIv2 :: %s.' % self._api_msg)
|
||||
|
||||
logger.api_debug('Tautulli APIv2 :: Cleaned kwargs: %s' % self._api_kwargs)
|
||||
|
||||
return self._api_kwargs
|
||||
|
||||
def get_logs(self, sort='', search='', order='desc', regex='', start=0, end=0, **kwargs):
|
||||
"""
|
||||
Get the Tautulli logs.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
None
|
||||
|
||||
Optional parameters:
|
||||
sort (str): "time", "thread", "msg", "loglevel"
|
||||
search (str): A string to search for
|
||||
order (str): "desc" or "asc"
|
||||
regex (str): A regex string to search for
|
||||
start (int): Row number to start from
|
||||
end (int): Row number to end at
|
||||
|
||||
Returns:
|
||||
json:
|
||||
[{"loglevel": "DEBUG",
|
||||
"msg": "Latest version is 2d10b0748c7fa2ee4cf59960c3d3fffc6aa9512b",
|
||||
"thread": "MainThread",
|
||||
"time": "2016-05-08 09:36:51 "
|
||||
},
|
||||
{...},
|
||||
{...}
|
||||
]
|
||||
```
|
||||
"""
|
||||
logfile = os.path.join(jellypy.CONFIG.LOG_DIR, logger.FILENAME)
|
||||
templog = []
|
||||
start = int(start)
|
||||
end = int(end)
|
||||
|
||||
if regex:
|
||||
logger.api_debug("Tautulli APIv2 :: Filtering log using regex '%s'" % regex)
|
||||
reg = re.compile(regex, flags=re.I)
|
||||
|
||||
with open(logfile, 'r', encoding='utf-8') as f:
|
||||
for line in f.readlines():
|
||||
temp_loglevel_and_time = None
|
||||
|
||||
try:
|
||||
temp_loglevel_and_time = line.split('- ')
|
||||
loglvl = temp_loglevel_and_time[1].split(' :')[0].strip()
|
||||
tl_tread = line.split(' :: ')
|
||||
if loglvl is None:
|
||||
msg = line.replace('\n', '')
|
||||
else:
|
||||
msg = line.split(' : ')[1].replace('\n', '')
|
||||
thread = tl_tread[1].split(' : ')[0]
|
||||
except IndexError:
|
||||
# We assume this is a traceback
|
||||
tl = (len(templog) - 1)
|
||||
templog[tl]['msg'] += helpers.sanitize(line.replace('\n', ''))
|
||||
continue
|
||||
|
||||
if len(line) > 1 and temp_loglevel_and_time is not None and loglvl in line:
|
||||
|
||||
d = {
|
||||
'time': temp_loglevel_and_time[0],
|
||||
'loglevel': loglvl,
|
||||
'msg': helpers.sanitize(msg.replace('\n', '')),
|
||||
'thread': thread
|
||||
}
|
||||
templog.append(d)
|
||||
|
||||
if order == 'desc':
|
||||
templog = templog[::-1]
|
||||
|
||||
if end > 0 or start > 0:
|
||||
logger.api_debug("Tautulli APIv2 :: Slicing the log from %s to %s" % (start, end))
|
||||
templog = templog[start:end]
|
||||
|
||||
if sort:
|
||||
logger.api_debug("Tautulli APIv2 :: Sorting log based on '%s'" % sort)
|
||||
templog = sorted(templog, key=lambda k: k[sort])
|
||||
|
||||
if search:
|
||||
logger.api_debug("Tautulli APIv2 :: Searching log values for '%s'" % search)
|
||||
tt = [d for d in templog for k, v in d.items() if search.lower() in v.lower()]
|
||||
|
||||
if len(tt):
|
||||
templog = tt
|
||||
|
||||
if regex:
|
||||
tt = []
|
||||
for l in templog:
|
||||
stringdict = ' '.join('{}{}'.format(k, v) for k, v in l.items())
|
||||
if reg.search(stringdict):
|
||||
tt.append(l)
|
||||
|
||||
if len(tt):
|
||||
templog = tt
|
||||
|
||||
return templog
|
||||
|
||||
def get_settings(self, key=''):
|
||||
""" Gets all settings from the config file.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
None
|
||||
|
||||
Optional parameters:
|
||||
key (str): Name of a config section to return
|
||||
|
||||
Returns:
|
||||
json:
|
||||
{"General": {"api_enabled": true, ...}
|
||||
"Advanced": {"cache_sizemb": "32", ...},
|
||||
...
|
||||
}
|
||||
```
|
||||
"""
|
||||
|
||||
interface_dir = os.path.join(jellypy.PROG_DIR, 'data/interfaces/')
|
||||
interface_list = [name for name in os.listdir(interface_dir) if
|
||||
os.path.isdir(os.path.join(interface_dir, name))]
|
||||
|
||||
conf = jellypy.CONFIG._config
|
||||
config = {}
|
||||
|
||||
# Truthify the dict
|
||||
for k, v in conf.items():
|
||||
if isinstance(v, dict):
|
||||
d = {}
|
||||
for kk, vv in v.items():
|
||||
if vv == '0' or vv == '1':
|
||||
d[kk] = bool(vv)
|
||||
else:
|
||||
d[kk] = vv
|
||||
config[k] = d
|
||||
if k == 'General':
|
||||
config[k]['interface'] = interface_dir
|
||||
config[k]['interface_list'] = interface_list
|
||||
|
||||
if key:
|
||||
return config.get(key)
|
||||
|
||||
return config
|
||||
|
||||
def sql(self, query=''):
|
||||
""" Query the Tautulli database with raw SQL. Automatically makes a backup of
|
||||
the database if the latest backup is older then 24h. `api_sql` must be
|
||||
manually enabled in the config file while Tautulli is shut down.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
query (str): The SQL query
|
||||
|
||||
Optional parameters:
|
||||
None
|
||||
|
||||
Returns:
|
||||
None
|
||||
```
|
||||
"""
|
||||
if not jellypy.CONFIG.API_SQL:
|
||||
self._api_msg = 'SQL not enabled for the API.'
|
||||
return
|
||||
|
||||
if not query:
|
||||
self._api_msg = 'No SQL query provided.'
|
||||
return
|
||||
|
||||
# allow the user to shoot them self
|
||||
# in the foot but not in the head..
|
||||
if not len(os.listdir(jellypy.CONFIG.BACKUP_DIR)):
|
||||
self.backup_db()
|
||||
else:
|
||||
# If the backup is less then 24 h old lets make a backup
|
||||
if not any(os.path.getctime(os.path.join(jellypy.CONFIG.BACKUP_DIR, file_)) > (time.time() - 86400)
|
||||
and file_.endswith('.db') for file_ in os.listdir(jellypy.CONFIG.BACKUP_DIR)):
|
||||
self.backup_db()
|
||||
|
||||
db = database.MonitorDatabase()
|
||||
rows = db.select(query)
|
||||
return rows
|
||||
|
||||
def backup_config(self):
|
||||
""" Create a manual backup of the `config.ini` file."""
|
||||
|
||||
data = config.make_backup()
|
||||
self._api_result_type = 'success' if data else 'error'
|
||||
|
||||
return data
|
||||
|
||||
def backup_db(self):
|
||||
""" Create a manual backup of the `jellypy.db` file."""
|
||||
|
||||
data = database.make_backup()
|
||||
self._api_result_type = 'success' if data else 'error'
|
||||
|
||||
return data
|
||||
|
||||
def restart(self, **kwargs):
|
||||
""" Restart Tautulli."""
|
||||
|
||||
jellypy.SIGNAL = 'restart'
|
||||
self._api_msg = 'Restarting Tautulli'
|
||||
self._api_result_type = 'success'
|
||||
|
||||
def update(self, **kwargs):
|
||||
""" Update Tautulli."""
|
||||
|
||||
jellypy.SIGNAL = 'update'
|
||||
self._api_msg = 'Updating Tautulli'
|
||||
self._api_result_type = 'success'
|
||||
|
||||
def refresh_libraries_list(self, **kwargs):
|
||||
""" Refresh the Tautulli libraries list."""
|
||||
data = libraries.refresh_libraries()
|
||||
self._api_result_type = 'success' if data else 'error'
|
||||
|
||||
return data
|
||||
|
||||
def refresh_users_list(self, **kwargs):
|
||||
""" Refresh the Tautulli users list."""
|
||||
data = users.refresh_users()
|
||||
self._api_result_type = 'success' if data else 'error'
|
||||
|
||||
return data
|
||||
|
||||
def register_device(self, device_id='', device_name='', friendly_name='', onesignal_id=None,
|
||||
min_version='', **kwargs):
|
||||
""" Registers the Tautulli Android App for notifications.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
device_id (str): The unique device identifier for the mobile device
|
||||
device_name (str): The device name of the mobile device
|
||||
|
||||
Optional parameters:
|
||||
friendly_name (str): A friendly name to identify the mobile device
|
||||
onesignal_id (str): The OneSignal id for the mobile device
|
||||
min_version (str): The minimum Tautulli version supported by the mobile device, e.g. v2.5.6
|
||||
|
||||
Returns:
|
||||
json:
|
||||
{"pms_identifier": "08u2phnlkdshf890bhdlksghnljsahgleikjfg9t",
|
||||
"pms_ip": "10.10.10.1",
|
||||
"pms_is_remote": 0,
|
||||
"pms_name": "Winterfell-Server",
|
||||
"pms_platform": "Windows",
|
||||
"pms_plexpass": 1,
|
||||
"pms_port": 32400,
|
||||
"pms_ssl": 0,
|
||||
"pms_url": "http://10.10.10.1:32400",
|
||||
"pms_url_manual": 0,
|
||||
"pms_version": "1.20.0.3133-fede5bdc7"
|
||||
"server_id": "2ce060c87958445d8399a7a0c5663755",
|
||||
"tautulli_install_type": "git",
|
||||
"tautulli_branch": "master",
|
||||
"tautulli_commit": "14b98a32e085d969f010f0249c3d2f660db50880",
|
||||
"tautulli_platform": "Windows",
|
||||
"tautulli_platform_device_name": "Winterfell-PC",
|
||||
"tautulli_platform_linux_distro": "",
|
||||
"tautulli_platform_release": "10",
|
||||
"tautulli_platform_version": "10.0.18362",
|
||||
"tautulli_python_version": "3.8.3"
|
||||
"tautulli_version": "v2.5.6",
|
||||
}
|
||||
```
|
||||
"""
|
||||
if not device_id:
|
||||
self._api_msg = 'Device registration failed: no device id provided.'
|
||||
self._api_result_type = 'error'
|
||||
return
|
||||
|
||||
elif not device_name:
|
||||
self._api_msg = 'Device registration failed: no device name provided.'
|
||||
self._api_result_type = 'error'
|
||||
return
|
||||
|
||||
elif min_version and helpers.version_to_tuple(min_version) > helpers.version_to_tuple(common.RELEASE):
|
||||
self._api_msg = 'Device registration failed: Tautulli version {} ' \
|
||||
'does not meet the minimum requirement of {}.'.format(common.RELEASE, min_version)
|
||||
self._api_result_type = 'error'
|
||||
return
|
||||
|
||||
## TODO: Temporary for backwards compatibility, assume device_id is onesignal_id
|
||||
if device_id and onesignal_id is None:
|
||||
onesignal_id = device_id
|
||||
|
||||
result = mobile_app.add_mobile_device(device_id=device_id,
|
||||
device_name=device_name,
|
||||
device_token=self._api_apikey,
|
||||
friendly_name=friendly_name,
|
||||
onesignal_id=onesignal_id)
|
||||
|
||||
if result:
|
||||
self._api_msg = 'Device registration successful.'
|
||||
self._api_result_type = 'success'
|
||||
|
||||
mobile_app.set_temp_device_token(True)
|
||||
|
||||
plex_server = plextv.get_server_resources(return_info=True)
|
||||
tautulli = jellypy.get_tautulli_info()
|
||||
|
||||
data = {"server_id": jellypy.CONFIG.PMS_UUID}
|
||||
data.update(plex_server)
|
||||
data.update(tautulli)
|
||||
|
||||
return data
|
||||
|
||||
else:
|
||||
self._api_msg = 'Device registration failed: database error.'
|
||||
self._api_result_type = 'error'
|
||||
|
||||
return
|
||||
|
||||
def notify(self, notifier_id='', subject='', body='', **kwargs):
|
||||
""" Send a notification using Tautulli.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
notifier_id (int): The ID number of the notification agent
|
||||
subject (str): The subject of the message
|
||||
body (str): The body of the message
|
||||
|
||||
Optional parameters:
|
||||
headers (str): The JSON headers for webhook notifications
|
||||
script_args (str): The arguments for script notifications
|
||||
|
||||
Returns:
|
||||
None
|
||||
```
|
||||
"""
|
||||
if not notifier_id:
|
||||
self._api_msg = 'Notification failed: no notifier id provided.'
|
||||
self._api_result_type = 'error'
|
||||
return
|
||||
|
||||
notifier = notifiers.get_notifier_config(notifier_id=notifier_id)
|
||||
|
||||
if not notifier:
|
||||
self._api_msg = 'Notification failed: invalid notifier_id provided %s.' % notifier_id
|
||||
self._api_result_type = 'error'
|
||||
return
|
||||
|
||||
logger.api_debug('Tautulli APIv2 :: Sending notification.')
|
||||
success = notification_handler.notify(notifier_id=notifier_id,
|
||||
notify_action='api',
|
||||
subject=subject,
|
||||
body=body,
|
||||
**kwargs)
|
||||
|
||||
if success:
|
||||
self._api_msg = 'Notification sent.'
|
||||
self._api_result_type = 'success'
|
||||
else:
|
||||
self._api_msg = 'Notification failed.'
|
||||
self._api_result_type = 'error'
|
||||
|
||||
return
|
||||
|
||||
def notify_newsletter(self, newsletter_id='', subject='', body='', message='', **kwargs):
|
||||
""" Send a newsletter using Tautulli.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
newsletter_id (int): The ID number of the newsletter agent
|
||||
|
||||
Optional parameters:
|
||||
subject (str): The subject of the newsletter
|
||||
body (str): The body of the newsletter
|
||||
message (str): The message of the newsletter
|
||||
|
||||
Returns:
|
||||
None
|
||||
```
|
||||
"""
|
||||
if not newsletter_id:
|
||||
self._api_msg = 'Newsletter failed: no newsletter id provided.'
|
||||
self._api_result_type = 'error'
|
||||
return
|
||||
|
||||
newsletter = newsletters.get_newsletter_config(newsletter_id=newsletter_id)
|
||||
|
||||
if not newsletter:
|
||||
self._api_msg = 'Newsletter failed: invalid newsletter_id provided %s.' % newsletter_id
|
||||
self._api_result_type = 'error'
|
||||
return
|
||||
|
||||
logger.api_debug('Tautulli APIv2 :: Sending newsletter.')
|
||||
success = newsletter_handler.notify(newsletter_id=newsletter_id,
|
||||
notify_action='api',
|
||||
subject=subject,
|
||||
body=body,
|
||||
message=message,
|
||||
**kwargs)
|
||||
|
||||
if success:
|
||||
self._api_msg = 'Newsletter sent.'
|
||||
self._api_result_type = 'success'
|
||||
else:
|
||||
self._api_msg = 'Newsletter failed.'
|
||||
self._api_result_type = 'error'
|
||||
|
||||
return
|
||||
|
||||
def _api_make_md(self):
|
||||
""" Tries to make a API.md to simplify the api docs. """
|
||||
|
||||
head = '''## General structure
|
||||
The API endpoint is
|
||||
```
|
||||
http://IP_ADDRESS:PORT + [/HTTP_ROOT] + /api/v2?apikey=$apikey&cmd=$command
|
||||
```
|
||||
|
||||
Example:
|
||||
```
|
||||
http://localhost:8181/api/v2?apikey=66198313a092496b8a725867d2223b5f&cmd=get_metadata&rating_key=153037
|
||||
```
|
||||
|
||||
Response example (default `json`)
|
||||
```
|
||||
{
|
||||
"response": {
|
||||
"data": [
|
||||
{
|
||||
"loglevel": "INFO",
|
||||
"msg": "Signal 2 caught, saving and exiting...",
|
||||
"thread": "MainThread",
|
||||
"time": "22-sep-2015 01:42:56 "
|
||||
}
|
||||
],
|
||||
"message": null,
|
||||
"result": "success"
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
General optional parameters:
|
||||
|
||||
out_type: "json" or "xml"
|
||||
callback: "pong"
|
||||
debug: 1
|
||||
```
|
||||
|
||||
## API methods'''
|
||||
|
||||
body = ''
|
||||
doc = self._api_docs(md=True)
|
||||
for k in sorted(doc):
|
||||
v = doc.get(k)
|
||||
body += '### %s\n' % k
|
||||
body += '' if not v else v + '\n'
|
||||
body += '\n\n'
|
||||
|
||||
result = head + '\n\n' + body
|
||||
return '<pre>' + result + '</pre>'
|
||||
|
||||
def get_apikey(self, username='', password=''):
|
||||
""" Get the apikey. Username and password are required
|
||||
if auth is enabled. Makes and saves the apikey if it does not exist.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
None
|
||||
|
||||
Optional parameters:
|
||||
username (str): Your Tautulli username
|
||||
password (str): Your Tautulli password
|
||||
|
||||
Returns:
|
||||
string: "apikey"
|
||||
```
|
||||
"""
|
||||
data = None
|
||||
apikey = hashlib.sha224(str(random.getrandbits(256)).encode('utf-8')).hexdigest()[0:32]
|
||||
if jellypy.CONFIG.HTTP_USERNAME and jellypy.CONFIG.HTTP_PASSWORD:
|
||||
authenticated = False
|
||||
if jellypy.CONFIG.HTTP_HASHED_PASSWORD and \
|
||||
username == jellypy.CONFIG.HTTP_USERNAME and check_hash(password, jellypy.CONFIG.HTTP_PASSWORD):
|
||||
authenticated = True
|
||||
elif not jellypy.CONFIG.HTTP_HASHED_PASSWORD and \
|
||||
username == jellypy.CONFIG.HTTP_USERNAME and password == jellypy.CONFIG.HTTP_PASSWORD:
|
||||
authenticated = True
|
||||
|
||||
if authenticated:
|
||||
if jellypy.CONFIG.API_KEY:
|
||||
data = jellypy.CONFIG.API_KEY
|
||||
else:
|
||||
data = apikey
|
||||
jellypy.CONFIG.API_KEY = apikey
|
||||
jellypy.CONFIG.write()
|
||||
else:
|
||||
self._api_msg = 'Authentication is enabled, please add the correct username and password to the parameters'
|
||||
else:
|
||||
if jellypy.CONFIG.API_KEY:
|
||||
data = jellypy.CONFIG.API_KEY
|
||||
else:
|
||||
# Make a apikey if the doesn't exist
|
||||
data = apikey
|
||||
jellypy.CONFIG.API_KEY = apikey
|
||||
jellypy.CONFIG.write()
|
||||
|
||||
return data
|
||||
|
||||
def _api_responds(self, result_type='error', data=None, msg=''):
|
||||
""" Formats the result to a predefined dict so we can change it the to
|
||||
the desired output by _api_out_as """
|
||||
|
||||
if data is None:
|
||||
data = {}
|
||||
return {"response": {"result": result_type, "message": msg, "data": data}}
|
||||
|
||||
def _api_out_as(self, out):
|
||||
""" Formats the response to the desired output """
|
||||
|
||||
if self._api_cmd == 'docs_md':
|
||||
return out['response']['data']
|
||||
|
||||
elif self._api_cmd and self._api_cmd.startswith('download_'):
|
||||
return out['response']['data']
|
||||
|
||||
elif self._api_cmd == 'pms_image_proxy':
|
||||
if 'return_hash' not in self._api_kwargs:
|
||||
cherrypy.response.headers['Content-Type'] = 'image/jpeg'
|
||||
return out['response']['data']
|
||||
|
||||
elif self._api_cmd == 'get_geoip_lookup':
|
||||
# Remove nested data and put error message inside data for backwards compatibility
|
||||
out['response']['data'] = out['response']['data'].get('data')
|
||||
if not out['response']['data']:
|
||||
out['response']['data'] = {'error': out['response']['message']}
|
||||
|
||||
if self._api_out_type == 'json':
|
||||
cherrypy.response.headers['Content-Type'] = 'application/json;charset=UTF-8'
|
||||
try:
|
||||
if self._api_debug:
|
||||
out = json.dumps(out, indent=4, sort_keys=True, ensure_ascii=False)
|
||||
else:
|
||||
out = json.dumps(out, ensure_ascii=False)
|
||||
if self._api_callback is not None:
|
||||
cherrypy.response.headers['Content-Type'] = 'application/javascript'
|
||||
# wrap with JSONP call if requested
|
||||
out = self._api_callback + '(' + out + ');'
|
||||
# if we fail to generate the output fake an error
|
||||
except Exception as e:
|
||||
logger.api_exception('Tautulli APIv2 :: ' + traceback.format_exc())
|
||||
self._api_response_code = 500
|
||||
out['message'] = traceback.format_exc()
|
||||
out['result'] = 'error'
|
||||
|
||||
elif self._api_out_type == 'xml':
|
||||
cherrypy.response.headers['Content-Type'] = 'application/xml;charset=UTF-8'
|
||||
try:
|
||||
out = xmltodict.unparse(out, pretty=True)
|
||||
except Exception as e:
|
||||
logger.api_error('Tautulli APIv2 :: Failed to parse xml result')
|
||||
self._api_response_code = 500
|
||||
try:
|
||||
out['message'] = e
|
||||
out['result'] = 'error'
|
||||
out = xmltodict.unparse(out, pretty=True)
|
||||
|
||||
except Exception as e:
|
||||
logger.api_error('Tautulli APIv2 :: Failed to parse xml result error message %s' % e)
|
||||
out = '''<?xml version="1.0" encoding="utf-8"?>
|
||||
<response>
|
||||
<message>%s</message>
|
||||
<data></data>
|
||||
<result>error</result>
|
||||
</response>
|
||||
''' % e
|
||||
|
||||
return out.encode('utf-8')
|
||||
|
||||
def _api_run(self, *args, **kwargs):
|
||||
""" handles the stuff from the handler """
|
||||
|
||||
# Make sure the device ID is not shown in the logs
|
||||
if kwargs.get('cmd') == 'register_device':
|
||||
if kwargs.get('device_id'):
|
||||
logger._BLACKLIST_WORDS.add(kwargs['device_id'])
|
||||
if kwargs.get('onesignal_id'):
|
||||
logger._BLACKLIST_WORDS.add(kwargs['onesignal_id'])
|
||||
|
||||
elif kwargs.get('cmd') == 'get_apikey':
|
||||
if kwargs.get('password'):
|
||||
logger._BLACKLIST_WORDS.add(kwargs['password'])
|
||||
|
||||
result = {}
|
||||
logger.api_debug('Tautulli APIv2 :: API called with kwargs: %s' % kwargs)
|
||||
|
||||
self._api_validate(**kwargs)
|
||||
|
||||
if self._api_cmd and self._api_authenticated:
|
||||
call = getattr(self, self._api_cmd)
|
||||
|
||||
# Profile is written to console.
|
||||
if self._api_profileme:
|
||||
from profilehooks import profile
|
||||
call = profile(call, immediate=True)
|
||||
|
||||
# We allow this to fail so we get a
|
||||
# traceback in the browser
|
||||
try:
|
||||
|
||||
result = call(**self._api_kwargs)
|
||||
except Exception as e:
|
||||
logger.api_error('Tautulli APIv2 :: Failed to run %s with %s: %s' % (self._api_cmd, self._api_kwargs, e))
|
||||
self._api_response_code = 500
|
||||
if self._api_debug:
|
||||
cherrypy.request.show_tracebacks = True
|
||||
# Reraise the exception so the traceback hits the browser
|
||||
raise
|
||||
self._api_msg = 'Check the logs for errors'
|
||||
|
||||
ret = None
|
||||
# The api decorated function can return different result types.
|
||||
# convert it to a list/dict before we change it to the users
|
||||
# wanted output
|
||||
try:
|
||||
if isinstance(result, (dict, list)):
|
||||
ret = result
|
||||
else:
|
||||
raise Exception
|
||||
except Exception:
|
||||
try:
|
||||
ret = json.loads(result)
|
||||
except (ValueError, TypeError):
|
||||
try:
|
||||
ret = xmltodict.parse(result, attr_prefix='')
|
||||
except:
|
||||
pass
|
||||
|
||||
# Fallback if we cant "parse the response"
|
||||
if ret is None:
|
||||
ret = result
|
||||
|
||||
if (ret is not None or self._api_result_type == 'success') and self._api_authenticated:
|
||||
# To allow override for restart etc
|
||||
# if the call returns some data we are gonna assume its a success
|
||||
self._api_result_type = 'success'
|
||||
self._api_response_code = 200
|
||||
|
||||
# Since some of them methods use a api like response for the ui
|
||||
# {result: error, message: 'Some shit happened'}
|
||||
if isinstance(ret, dict):
|
||||
if ret.get('message'):
|
||||
self._api_msg = ret.pop('message', None)
|
||||
|
||||
if ret.get('result'):
|
||||
self._api_result_type = ret.pop('result', None)
|
||||
|
||||
if self._api_result_type == 'success' and not self._api_response_code:
|
||||
self._api_response_code = 200
|
||||
elif self._api_result_type == 'error' and not self._api_response_code:
|
||||
self._api_response_code = 400
|
||||
|
||||
if not self._api_response_code:
|
||||
self._api_response_code = 500
|
||||
|
||||
cherrypy.response.status = self._api_response_code
|
||||
return self._api_out_as(self._api_responds(result_type=self._api_result_type, msg=self._api_msg, data=ret))
|
Reference in New Issue
Block a user