mirror of
https://github.com/Screenly/Anthias.git
synced 2025-12-30 17:58:17 -05:00
1897 lines
56 KiB
Python
Executable File
1897 lines
56 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
__author__ = "Screenly, Inc"
|
|
__copyright__ = "Copyright 2012-2023, Screenly, Inc"
|
|
__license__ = "Dual License: GPLv2 and Commercial License"
|
|
|
|
import json
|
|
import pydbus
|
|
import psutil
|
|
import re
|
|
import sh
|
|
import shutil
|
|
import time
|
|
import os
|
|
|
|
import traceback
|
|
import yaml
|
|
import uuid
|
|
from base64 import b64encode
|
|
from celery import Celery
|
|
from datetime import datetime, timedelta
|
|
from dateutil import parser as date_parser
|
|
from functools import wraps
|
|
from hurry.filesize import size
|
|
from mimetypes import guess_type, guess_extension
|
|
from os import getenv, listdir, makedirs, mkdir, path, remove, rename, statvfs, stat, walk
|
|
from retry.api import retry_call
|
|
from subprocess import check_output
|
|
from urlparse import urlparse
|
|
|
|
from flask import Flask, escape, make_response, render_template, request, send_from_directory, url_for, jsonify
|
|
from flask_cors import CORS
|
|
from flask_restful_swagger_2 import Api, Resource, Schema, swagger
|
|
from flask_swagger_ui import get_swaggerui_blueprint
|
|
|
|
from gunicorn.app.base import Application
|
|
from werkzeug.wrappers import Request
|
|
|
|
from lib import assets_helper
|
|
from lib import backup_helper
|
|
from lib import db
|
|
from lib import diagnostics
|
|
from lib import queries
|
|
from lib import raspberry_pi_helper
|
|
|
|
from lib.github import is_up_to_date
|
|
from lib.auth import authorized
|
|
|
|
from lib.utils import (
|
|
download_video_from_youtube, json_dump,
|
|
generate_perfect_paper_password, is_docker,
|
|
get_active_connections, remove_connection,
|
|
get_node_ip, get_node_mac_address,
|
|
get_video_duration,
|
|
is_balena_app, is_demo_node,
|
|
shutdown_via_balena_supervisor, reboot_via_balena_supervisor,
|
|
string_to_bool,
|
|
connect_to_redis,
|
|
url_fails,
|
|
validate_url,
|
|
)
|
|
|
|
from settings import CONFIGURABLE_SETTINGS, DEFAULTS, LISTEN, PORT, settings, ZmqPublisher, ZmqCollector
|
|
|
|
HOME = getenv('HOME')
|
|
CELERY_RESULT_BACKEND = getenv('CELERY_RESULT_BACKEND', 'redis://localhost:6379/0')
|
|
CELERY_BROKER_URL = getenv('CELERY_BROKER_URL', 'redis://localhost:6379/0')
|
|
CELERY_TASK_RESULT_EXPIRES = timedelta(hours=6)
|
|
|
|
app = Flask(__name__)
|
|
app.debug = string_to_bool(os.getenv('DEBUG', 'False'))
|
|
|
|
CORS(app)
|
|
api = Api(app, api_version="v1", title="Screenly OSE API")
|
|
|
|
r = connect_to_redis()
|
|
celery = Celery(
|
|
app.name,
|
|
backend=CELERY_RESULT_BACKEND,
|
|
broker=CELERY_BROKER_URL,
|
|
result_expires=CELERY_TASK_RESULT_EXPIRES
|
|
)
|
|
|
|
|
|
################################
|
|
# Celery tasks
|
|
################################
|
|
|
|
@celery.on_after_configure.connect
|
|
def setup_periodic_tasks(sender, **kwargs):
|
|
# Calls cleanup() every hour.
|
|
sender.add_periodic_task(3600, cleanup.s(), name='cleanup')
|
|
sender.add_periodic_task(3600, cleanup_usb_assets.s(), name='cleanup_usb_assets')
|
|
sender.add_periodic_task(60*5, get_display_power.s(), name='display_power')
|
|
|
|
|
|
@celery.task
|
|
def get_display_power():
|
|
r.set('display_power', diagnostics.get_display_power())
|
|
r.expire('display_power', 3600)
|
|
|
|
|
|
@celery.task
|
|
def cleanup():
|
|
sh.find(path.join(HOME, 'screenly_assets'), '-name', '*.tmp', '-delete')
|
|
|
|
|
|
@celery.task
|
|
def reboot_screenly():
|
|
"""
|
|
Background task to reboot Screenly-OSE.
|
|
"""
|
|
if is_balena_app():
|
|
retry_call(reboot_via_balena_supervisor, tries=5, delay=1)
|
|
else:
|
|
r.publish('hostcmd', 'reboot')
|
|
|
|
|
|
@celery.task
|
|
def shutdown_screenly():
|
|
"""
|
|
Background task to shutdown Screenly-OSE.
|
|
"""
|
|
if is_balena_app():
|
|
retry_call(shutdown_via_balena_supervisor, tries=5, delay=1)
|
|
else:
|
|
r.publish('hostcmd', 'shutdown')
|
|
|
|
|
|
@celery.task
|
|
def append_usb_assets(mountpoint):
|
|
"""
|
|
@TODO. Fix me. This will not work in Docker.
|
|
"""
|
|
settings.load()
|
|
|
|
datetime_now = datetime.now()
|
|
usb_assets_settings = {
|
|
'activate': False,
|
|
'copy': False,
|
|
'start_date': datetime_now,
|
|
'end_date': datetime_now + timedelta(days=7),
|
|
'duration': settings['default_duration']
|
|
}
|
|
|
|
for root, _, filenames in walk(mountpoint):
|
|
if 'usb_assets_key.yaml' in filenames:
|
|
with open("%s/%s" % (root, 'usb_assets_key.yaml'), 'r') as yaml_file:
|
|
usb_file_settings = yaml.load(yaml_file).get('screenly')
|
|
if usb_file_settings.get('key') == settings['usb_assets_key']:
|
|
if usb_file_settings.get('activate'):
|
|
usb_assets_settings.update({
|
|
'activate': usb_file_settings.get('activate')
|
|
})
|
|
if usb_file_settings.get('copy'):
|
|
usb_assets_settings.update({
|
|
'copy': usb_file_settings.get('copy')
|
|
})
|
|
if usb_file_settings.get('start_date'):
|
|
ts = time.mktime(datetime.strptime(usb_file_settings.get('start_date'), "%m/%d/%Y").timetuple())
|
|
usb_assets_settings.update({
|
|
'start_date': datetime.utcfromtimestamp(ts)
|
|
})
|
|
if usb_file_settings.get('end_date'):
|
|
ts = time.mktime(datetime.strptime(usb_file_settings.get('end_date'), "%m/%d/%Y").timetuple())
|
|
usb_assets_settings.update({
|
|
'end_date': datetime.utcfromtimestamp(ts)
|
|
})
|
|
if usb_file_settings.get('duration'):
|
|
usb_assets_settings.update({
|
|
'duration': usb_file_settings.get('duration')
|
|
})
|
|
|
|
files = ['%s/%s' % (root, y) for root, _, filenames in walk(mountpoint) for y in filenames]
|
|
with db.conn(settings['database']) as conn:
|
|
for filepath in files:
|
|
asset = prepare_usb_asset(filepath, **usb_assets_settings)
|
|
if asset:
|
|
assets_helper.create(conn, asset)
|
|
|
|
break
|
|
|
|
|
|
@celery.task
|
|
def remove_usb_assets(mountpoint):
|
|
"""
|
|
@TODO. Fix me. This will not work in Docker.
|
|
"""
|
|
settings.load()
|
|
with db.conn(settings['database']) as conn:
|
|
for asset in assets_helper.read(conn):
|
|
if asset['uri'].startswith(mountpoint):
|
|
assets_helper.delete(conn, asset['asset_id'])
|
|
|
|
|
|
@celery.task
|
|
def cleanup_usb_assets(media_dir='/media'):
|
|
"""
|
|
@TODO. Fix me. This will not work in Docker.
|
|
"""
|
|
settings.load()
|
|
mountpoints = ['%s/%s' % (media_dir, x) for x in listdir(media_dir) if path.isdir('%s/%s' % (media_dir, x))]
|
|
with db.conn(settings['database']) as conn:
|
|
for asset in assets_helper.read(conn):
|
|
if asset['uri'].startswith(media_dir):
|
|
location = re.search(r'^(/\w+/\w+[^/])', asset['uri'])
|
|
if location:
|
|
if location.group() not in mountpoints:
|
|
assets_helper.delete(conn, asset['asset_id'])
|
|
|
|
|
|
################################
|
|
# Utilities
|
|
################################
|
|
|
|
|
|
@api.representation('application/json')
|
|
def output_json(data, code, headers=None):
|
|
response = make_response(json_dump(data), code)
|
|
response.headers.extend(headers or {})
|
|
return response
|
|
|
|
|
|
def api_error(error):
|
|
return make_response(json_dump({'error': error}), 500)
|
|
|
|
|
|
def template(template_name, **context):
|
|
"""Screenly template response generator. Shares the
|
|
same function signature as Flask's render_template() method
|
|
but also injects some global context."""
|
|
|
|
# Add global contexts
|
|
context['date_format'] = settings['date_format']
|
|
context['default_duration'] = settings['default_duration']
|
|
context['default_streaming_duration'] = settings['default_streaming_duration']
|
|
context['template_settings'] = {
|
|
'imports': ['from lib.utils import template_handle_unicode'],
|
|
'default_filters': ['template_handle_unicode'],
|
|
}
|
|
context['up_to_date'] = is_up_to_date()
|
|
context['use_24_hour_clock'] = settings['use_24_hour_clock']
|
|
|
|
return render_template(template_name, context=context)
|
|
|
|
|
|
################################
|
|
# Models
|
|
################################
|
|
|
|
class AssetModel(Schema):
|
|
type = 'object'
|
|
properties = {
|
|
'asset_id': {'type': 'string'},
|
|
'name': {'type': 'string'},
|
|
'uri': {'type': 'string'},
|
|
'start_date': {
|
|
'type': 'string',
|
|
'format': 'date-time'
|
|
},
|
|
'end_date': {
|
|
'type': 'string',
|
|
'format': 'date-time'
|
|
},
|
|
'duration': {'type': 'string'},
|
|
'mimetype': {'type': 'string'},
|
|
'is_active': {
|
|
'type': 'integer',
|
|
'format': 'int64',
|
|
},
|
|
'is_enabled': {
|
|
'type': 'integer',
|
|
'format': 'int64',
|
|
},
|
|
'is_processing': {
|
|
'type': 'integer',
|
|
'format': 'int64',
|
|
},
|
|
'nocache': {
|
|
'type': 'integer',
|
|
'format': 'int64',
|
|
},
|
|
'play_order': {
|
|
'type': 'integer',
|
|
'format': 'int64',
|
|
},
|
|
'skip_asset_check': {
|
|
'type': 'integer',
|
|
'format': 'int64',
|
|
}
|
|
}
|
|
|
|
|
|
class AssetRequestModel(Schema):
|
|
type = 'object'
|
|
properties = {
|
|
'name': {'type': 'string'},
|
|
'uri': {'type': 'string'},
|
|
'start_date': {
|
|
'type': 'string',
|
|
'format': 'date-time'
|
|
},
|
|
'end_date': {
|
|
'type': 'string',
|
|
'format': 'date-time'
|
|
},
|
|
'duration': {'type': 'string'},
|
|
'mimetype': {'type': 'string'},
|
|
'is_enabled': {
|
|
'type': 'integer',
|
|
'format': 'int64',
|
|
},
|
|
'nocache': {
|
|
'type': 'integer',
|
|
'format': 'int64',
|
|
},
|
|
'play_order': {
|
|
'type': 'integer',
|
|
'format': 'int64',
|
|
},
|
|
'skip_asset_check': {
|
|
'type': 'integer',
|
|
'format': 'int64',
|
|
}
|
|
}
|
|
required = ['name', 'uri', 'mimetype', 'is_enabled', 'start_date', 'end_date']
|
|
|
|
|
|
class AssetContentModel(Schema):
|
|
type = 'object'
|
|
properties = {
|
|
'type': {'type': 'string'},
|
|
'url': {'type': 'string'},
|
|
'filename': {'type': 'string'},
|
|
'mimetype': {'type': 'string'},
|
|
'content': {
|
|
'type': 'string',
|
|
'format': 'byte'
|
|
},
|
|
}
|
|
required = ['type', 'filename']
|
|
|
|
|
|
class AssetPropertiesModel(Schema):
|
|
type = 'object'
|
|
properties = {
|
|
'name': {'type': 'string'},
|
|
'start_date': {
|
|
'type': 'string',
|
|
'format': 'date-time'
|
|
},
|
|
'end_date': {
|
|
'type': 'string',
|
|
'format': 'date-time'
|
|
},
|
|
'duration': {'type': 'string'},
|
|
'is_active': {
|
|
'type': 'integer',
|
|
'format': 'int64',
|
|
},
|
|
'is_enabled': {
|
|
'type': 'integer',
|
|
'format': 'int64',
|
|
},
|
|
'nocache': {
|
|
'type': 'integer',
|
|
'format': 'int64',
|
|
},
|
|
'play_order': {
|
|
'type': 'integer',
|
|
'format': 'int64',
|
|
},
|
|
'skip_asset_check': {
|
|
'type': 'integer',
|
|
'format': 'int64',
|
|
}
|
|
}
|
|
|
|
|
|
################################
|
|
# API
|
|
################################
|
|
|
|
def prepare_asset(request, unique_name=False):
|
|
req = Request(request.environ)
|
|
data = None
|
|
|
|
# For backward compatibility
|
|
try:
|
|
data = json.loads(req.data)
|
|
except ValueError:
|
|
data = json.loads(req.form['model'])
|
|
except TypeError:
|
|
data = json.loads(req.form['model'])
|
|
|
|
def get(key):
|
|
val = data.get(key, '')
|
|
if isinstance(val, unicode):
|
|
return val.strip()
|
|
elif isinstance(val, basestring):
|
|
return val.strip().decode('utf-8')
|
|
else:
|
|
return val
|
|
|
|
if not all([get('name'), get('uri'), get('mimetype')]):
|
|
raise Exception("Not enough information provided. Please specify 'name', 'uri', and 'mimetype'.")
|
|
|
|
name = escape(get('name'))
|
|
if unique_name:
|
|
with db.conn(settings['database']) as conn:
|
|
names = assets_helper.get_names_of_assets(conn)
|
|
if name in names:
|
|
i = 1
|
|
while True:
|
|
new_name = '%s-%i' % (name, i)
|
|
if new_name in names:
|
|
i += 1
|
|
else:
|
|
name = new_name
|
|
break
|
|
|
|
asset = {
|
|
'name': name,
|
|
'mimetype': get('mimetype'),
|
|
'asset_id': get('asset_id'),
|
|
'is_enabled': get('is_enabled'),
|
|
'is_processing': get('is_processing'),
|
|
'nocache': get('nocache'),
|
|
}
|
|
|
|
uri = escape(get('uri').encode('utf-8'))
|
|
|
|
if uri.startswith('/'):
|
|
if not path.isfile(uri):
|
|
raise Exception("Invalid file path. Failed to add asset.")
|
|
else:
|
|
if not validate_url(uri):
|
|
raise Exception("Invalid URL. Failed to add asset.")
|
|
|
|
if not asset['asset_id']:
|
|
asset['asset_id'] = uuid.uuid4().hex
|
|
if uri.startswith('/'):
|
|
rename(uri, path.join(settings['assetdir'], asset['asset_id']))
|
|
uri = path.join(settings['assetdir'], asset['asset_id'])
|
|
|
|
if 'youtube_asset' in asset['mimetype']:
|
|
uri, asset['name'], asset['duration'] = download_video_from_youtube(uri, asset['asset_id'])
|
|
asset['mimetype'] = 'video'
|
|
asset['is_processing'] = 1
|
|
|
|
asset['uri'] = uri
|
|
|
|
if "video" in asset['mimetype']:
|
|
if get('duration') == 'N/A' or int(get('duration')) == 0:
|
|
asset['duration'] = int(get_video_duration(uri).total_seconds())
|
|
else:
|
|
# Crashes if it's not an int. We want that.
|
|
asset['duration'] = int(get('duration'))
|
|
|
|
asset['skip_asset_check'] = int(get('skip_asset_check')) if int(get('skip_asset_check')) else 0
|
|
|
|
# parse date via python-dateutil and remove timezone info
|
|
if get('start_date'):
|
|
asset['start_date'] = date_parser.parse(get('start_date')).replace(tzinfo=None)
|
|
else:
|
|
asset['start_date'] = ""
|
|
|
|
if get('end_date'):
|
|
asset['end_date'] = date_parser.parse(get('end_date')).replace(tzinfo=None)
|
|
else:
|
|
asset['end_date'] = ""
|
|
|
|
return asset
|
|
|
|
|
|
def prepare_asset_v1_2(request_environ, asset_id=None, unique_name=False):
|
|
data = json.loads(request_environ.data)
|
|
|
|
def get(key):
|
|
val = data.get(key, '')
|
|
if isinstance(val, unicode):
|
|
return val.strip()
|
|
elif isinstance(val, basestring):
|
|
return val.strip().decode('utf-8')
|
|
else:
|
|
return val
|
|
|
|
if not all([get('name'),
|
|
get('uri'),
|
|
get('mimetype'),
|
|
str(get('is_enabled')),
|
|
get('start_date'),
|
|
get('end_date')]):
|
|
raise Exception(
|
|
"Not enough information provided. Please specify 'name', 'uri', 'mimetype', 'is_enabled', 'start_date' and 'end_date'.")
|
|
|
|
ampfix = "&"
|
|
name = escape(get('name').replace(ampfix, '&'))
|
|
if unique_name:
|
|
with db.conn(settings['database']) as conn:
|
|
names = assets_helper.get_names_of_assets(conn)
|
|
if name in names:
|
|
i = 1
|
|
while True:
|
|
new_name = '%s-%i' % (name, i)
|
|
if new_name in names:
|
|
i += 1
|
|
else:
|
|
name = new_name
|
|
break
|
|
|
|
asset = {
|
|
'name': name,
|
|
'mimetype': get('mimetype'),
|
|
'is_enabled': get('is_enabled'),
|
|
'nocache': get('nocache')
|
|
}
|
|
|
|
uri = (get('uri')).replace(ampfix, '&').replace('<', '<').replace('>', '>').replace('\'', ''').replace('\"', '"')
|
|
|
|
if uri.startswith('/'):
|
|
if not path.isfile(uri):
|
|
raise Exception("Invalid file path. Failed to add asset.")
|
|
else:
|
|
if not validate_url(uri):
|
|
raise Exception("Invalid URL. Failed to add asset.")
|
|
|
|
if not asset_id:
|
|
asset['asset_id'] = uuid.uuid4().hex
|
|
|
|
if not asset_id and uri.startswith('/'):
|
|
new_uri = "{}{}".format(path.join(settings['assetdir'], asset['asset_id']), get('ext'))
|
|
rename(uri, new_uri)
|
|
uri = new_uri
|
|
|
|
if 'youtube_asset' in asset['mimetype']:
|
|
uri, asset['name'], asset['duration'] = download_video_from_youtube(uri, asset['asset_id'])
|
|
asset['mimetype'] = 'video'
|
|
asset['is_processing'] = 1
|
|
|
|
asset['uri'] = uri
|
|
|
|
if "video" in asset['mimetype']:
|
|
if get('duration') == 'N/A' or int(get('duration')) == 0:
|
|
asset['duration'] = int(get_video_duration(uri).total_seconds())
|
|
elif get('duration'):
|
|
# Crashes if it's not an int. We want that.
|
|
asset['duration'] = int(get('duration'))
|
|
else:
|
|
asset['duration'] = 10
|
|
|
|
asset['play_order'] = get('play_order') if get('play_order') else 0
|
|
|
|
asset['skip_asset_check'] = int(get('skip_asset_check')) if int(get('skip_asset_check')) else 0
|
|
|
|
# parse date via python-dateutil and remove timezone info
|
|
asset['start_date'] = date_parser.parse(get('start_date')).replace(tzinfo=None)
|
|
asset['end_date'] = date_parser.parse(get('end_date')).replace(tzinfo=None)
|
|
|
|
return asset
|
|
|
|
|
|
def prepare_usb_asset(filepath, **kwargs):
|
|
filetype = guess_type(filepath)[0]
|
|
|
|
if not filetype:
|
|
return
|
|
|
|
filetype = filetype.split('/')[0]
|
|
|
|
if filetype not in ['image', 'video']:
|
|
return
|
|
|
|
asset_id = uuid.uuid4().hex
|
|
asset_name = path.basename(filepath)
|
|
duration = int(get_video_duration(filepath).total_seconds()) if "video" == filetype else int(kwargs['duration'])
|
|
|
|
if kwargs['copy']:
|
|
shutil.copy(filepath, path.join(settings['assetdir'], asset_id))
|
|
filepath = path.join(settings['assetdir'], asset_id)
|
|
|
|
return {
|
|
'asset_id': asset_id,
|
|
'duration': duration,
|
|
'end_date': kwargs['end_date'],
|
|
'is_active': 1,
|
|
'is_enabled': kwargs['activate'],
|
|
'is_processing': 0,
|
|
'mimetype': filetype,
|
|
'name': asset_name,
|
|
'nocache': 0,
|
|
'play_order': 0,
|
|
'skip_asset_check': 0,
|
|
'start_date': kwargs['start_date'],
|
|
'uri': filepath,
|
|
}
|
|
|
|
|
|
def prepare_default_asset(**kwargs):
|
|
if kwargs['mimetype'] not in ['image', 'video', 'webpage']:
|
|
return
|
|
|
|
asset_id = 'default_{}'.format(uuid.uuid4().hex)
|
|
duration = int(get_video_duration(kwargs['uri']).total_seconds()) if "video" == kwargs['mimetype'] else kwargs['duration']
|
|
|
|
return {
|
|
'asset_id': asset_id,
|
|
'duration': duration,
|
|
'end_date': kwargs['end_date'],
|
|
'is_active': 1,
|
|
'is_enabled': True,
|
|
'is_processing': 0,
|
|
'mimetype': kwargs['mimetype'],
|
|
'name': kwargs['name'],
|
|
'nocache': 0,
|
|
'play_order': 0,
|
|
'skip_asset_check': 0,
|
|
'start_date': kwargs['start_date'],
|
|
'uri': kwargs['uri']
|
|
}
|
|
|
|
|
|
def add_default_assets():
|
|
settings.load()
|
|
|
|
datetime_now = datetime.now()
|
|
default_asset_settings = {
|
|
'start_date': datetime_now,
|
|
'end_date': datetime_now.replace(year=datetime_now.year + 6),
|
|
'duration': settings['default_duration']
|
|
}
|
|
|
|
default_assets_yaml = path.join(HOME, '.screenly/default_assets.yml')
|
|
|
|
with open(default_assets_yaml, 'r') as yaml_file:
|
|
default_assets = yaml.safe_load(yaml_file).get('assets')
|
|
with db.conn(settings['database']) as conn:
|
|
for default_asset in default_assets:
|
|
default_asset_settings.update({
|
|
'name': default_asset.get('name'),
|
|
'uri': default_asset.get('uri'),
|
|
'mimetype': default_asset.get('mimetype')
|
|
})
|
|
asset = prepare_default_asset(**default_asset_settings)
|
|
if asset:
|
|
assets_helper.create(conn, asset)
|
|
|
|
|
|
def remove_default_assets():
|
|
settings.load()
|
|
with db.conn(settings['database']) as conn:
|
|
for asset in assets_helper.read(conn):
|
|
if asset['asset_id'].startswith('default_'):
|
|
assets_helper.delete(conn, asset['asset_id'])
|
|
|
|
|
|
def update_asset(asset, data):
|
|
for key, value in data.items():
|
|
|
|
if key in ['asset_id', 'is_processing', 'mimetype', 'uri'] or key not in asset:
|
|
continue
|
|
|
|
if key in ['start_date', 'end_date']:
|
|
value = date_parser.parse(value).replace(tzinfo=None)
|
|
|
|
if key in ['play_order', 'skip_asset_check', 'is_enabled', 'is_active', 'nocache']:
|
|
value = int(value)
|
|
|
|
if key == 'duration':
|
|
if "video" not in asset['mimetype']:
|
|
continue
|
|
value = int(value)
|
|
|
|
asset.update({key: value})
|
|
|
|
|
|
# api view decorator. handles errors
|
|
def api_response(view):
|
|
@wraps(view)
|
|
def api_view(*args, **kwargs):
|
|
try:
|
|
return view(*args, **kwargs)
|
|
except Exception as e:
|
|
traceback.print_exc()
|
|
return api_error(unicode(e))
|
|
|
|
return api_view
|
|
|
|
|
|
class Assets(Resource):
|
|
method_decorators = [authorized]
|
|
|
|
@swagger.doc({
|
|
'responses': {
|
|
'200': {
|
|
'description': 'List of assets',
|
|
'schema': {
|
|
'type': 'array',
|
|
'items': AssetModel
|
|
|
|
}
|
|
}
|
|
}
|
|
})
|
|
def get(self):
|
|
with db.conn(settings['database']) as conn:
|
|
assets = assets_helper.read(conn)
|
|
return assets
|
|
|
|
@api_response
|
|
@swagger.doc({
|
|
'parameters': [
|
|
{
|
|
'name': 'model',
|
|
'in': 'formData',
|
|
'type': 'string',
|
|
'description':
|
|
'''
|
|
Yes, that is just a string of JSON not JSON itself it will be parsed on the other end.
|
|
Content-Type: application/x-www-form-urlencoded
|
|
model: "{
|
|
"name": "Website",
|
|
"mimetype": "webpage",
|
|
"uri": "http://example.com",
|
|
"is_active": 0,
|
|
"start_date": "2017-02-02T00:33:00.000Z",
|
|
"end_date": "2017-03-01T00:33:00.000Z",
|
|
"duration": "10",
|
|
"is_enabled": 0,
|
|
"is_processing": 0,
|
|
"nocache": 0,
|
|
"play_order": 0,
|
|
"skip_asset_check": 0
|
|
}"
|
|
'''
|
|
}
|
|
],
|
|
'responses': {
|
|
'201': {
|
|
'description': 'Asset created',
|
|
'schema': AssetModel
|
|
}
|
|
}
|
|
})
|
|
def post(self):
|
|
asset = prepare_asset(request)
|
|
if url_fails(asset['uri']):
|
|
raise Exception("Could not retrieve file. Check the asset URL.")
|
|
with db.conn(settings['database']) as conn:
|
|
return assets_helper.create(conn, asset), 201
|
|
|
|
|
|
class Asset(Resource):
|
|
method_decorators = [api_response, authorized]
|
|
|
|
@swagger.doc({
|
|
'parameters': [
|
|
{
|
|
'name': 'asset_id',
|
|
'type': 'string',
|
|
'in': 'path',
|
|
'description': 'id of an asset'
|
|
}
|
|
],
|
|
'responses': {
|
|
'200': {
|
|
'description': 'Asset',
|
|
'schema': AssetModel
|
|
}
|
|
}
|
|
})
|
|
def get(self, asset_id):
|
|
with db.conn(settings['database']) as conn:
|
|
return assets_helper.read(conn, asset_id)
|
|
|
|
@swagger.doc({
|
|
'parameters': [
|
|
{
|
|
'name': 'asset_id',
|
|
'type': 'string',
|
|
'in': 'path',
|
|
'description': 'id of an asset'
|
|
},
|
|
{
|
|
'name': 'model',
|
|
'in': 'formData',
|
|
'type': 'string',
|
|
'description':
|
|
'''
|
|
Content-Type: application/x-www-form-urlencoded
|
|
model: "{
|
|
"asset_id": "793406aa1fd34b85aa82614004c0e63a",
|
|
"name": "Website",
|
|
"mimetype": "webpage",
|
|
"uri": "http://example.com",
|
|
"is_active": 0,
|
|
"start_date": "2017-02-02T00:33:00.000Z",
|
|
"end_date": "2017-03-01T00:33:00.000Z",
|
|
"duration": "10",
|
|
"is_enabled": 0,
|
|
"is_processing": 0,
|
|
"nocache": 0,
|
|
"play_order": 0,
|
|
"skip_asset_check": 0
|
|
}"
|
|
'''
|
|
}
|
|
],
|
|
'responses': {
|
|
'200': {
|
|
'description': 'Asset updated',
|
|
'schema': AssetModel
|
|
}
|
|
}
|
|
})
|
|
def put(self, asset_id):
|
|
with db.conn(settings['database']) as conn:
|
|
return assets_helper.update(conn, asset_id, prepare_asset(request))
|
|
|
|
@swagger.doc({
|
|
'parameters': [
|
|
{
|
|
'name': 'asset_id',
|
|
'type': 'string',
|
|
'in': 'path',
|
|
'description': 'id of an asset'
|
|
},
|
|
],
|
|
'responses': {
|
|
'204': {
|
|
'description': 'Deleted'
|
|
}
|
|
}
|
|
})
|
|
def delete(self, asset_id):
|
|
with db.conn(settings['database']) as conn:
|
|
asset = assets_helper.read(conn, asset_id)
|
|
try:
|
|
if asset['uri'].startswith(settings['assetdir']):
|
|
remove(asset['uri'])
|
|
except OSError:
|
|
pass
|
|
assets_helper.delete(conn, asset_id)
|
|
return '', 204 # return an OK with no content
|
|
|
|
|
|
class AssetsV1_1(Resource):
|
|
method_decorators = [authorized]
|
|
|
|
@swagger.doc({
|
|
'responses': {
|
|
'200': {
|
|
'description': 'List of assets',
|
|
'schema': {
|
|
'type': 'array',
|
|
'items': AssetModel
|
|
|
|
}
|
|
}
|
|
}
|
|
})
|
|
def get(self):
|
|
with db.conn(settings['database']) as conn:
|
|
assets = assets_helper.read(conn)
|
|
return assets
|
|
|
|
@api_response
|
|
@swagger.doc({
|
|
'parameters': [
|
|
{
|
|
'in': 'body',
|
|
'name': 'model',
|
|
'description': 'Adds a asset',
|
|
'schema': AssetModel,
|
|
'required': True
|
|
}
|
|
],
|
|
'responses': {
|
|
'201': {
|
|
'description': 'Asset created',
|
|
'schema': AssetModel
|
|
}
|
|
}
|
|
})
|
|
def post(self):
|
|
asset = prepare_asset(request, unique_name=True)
|
|
if url_fails(asset['uri']):
|
|
raise Exception("Could not retrieve file. Check the asset URL.")
|
|
with db.conn(settings['database']) as conn:
|
|
return assets_helper.create(conn, asset), 201
|
|
|
|
|
|
class AssetV1_1(Resource):
|
|
method_decorators = [api_response, authorized]
|
|
|
|
@swagger.doc({
|
|
'parameters': [
|
|
{
|
|
'name': 'asset_id',
|
|
'type': 'string',
|
|
'in': 'path',
|
|
'description': 'id of an asset'
|
|
}
|
|
],
|
|
'responses': {
|
|
'200': {
|
|
'description': 'Asset',
|
|
'schema': AssetModel
|
|
}
|
|
}
|
|
})
|
|
def get(self, asset_id):
|
|
with db.conn(settings['database']) as conn:
|
|
return assets_helper.read(conn, asset_id)
|
|
|
|
@swagger.doc({
|
|
'parameters': [
|
|
{
|
|
'name': 'asset_id',
|
|
'type': 'string',
|
|
'in': 'path',
|
|
'description': 'id of an asset',
|
|
'required': True
|
|
},
|
|
{
|
|
'in': 'body',
|
|
'name': 'model',
|
|
'description': 'Adds an asset',
|
|
'schema': AssetModel,
|
|
'required': True
|
|
}
|
|
],
|
|
'responses': {
|
|
'200': {
|
|
'description': 'Asset updated',
|
|
'schema': AssetModel
|
|
}
|
|
}
|
|
})
|
|
def put(self, asset_id):
|
|
with db.conn(settings['database']) as conn:
|
|
return assets_helper.update(conn, asset_id, prepare_asset(request))
|
|
|
|
@swagger.doc({
|
|
'parameters': [
|
|
{
|
|
'name': 'asset_id',
|
|
'type': 'string',
|
|
'in': 'path',
|
|
'description': 'id of an asset',
|
|
'required': True
|
|
|
|
},
|
|
],
|
|
'responses': {
|
|
'204': {
|
|
'description': 'Deleted'
|
|
}
|
|
}
|
|
})
|
|
def delete(self, asset_id):
|
|
with db.conn(settings['database']) as conn:
|
|
asset = assets_helper.read(conn, asset_id)
|
|
try:
|
|
if asset['uri'].startswith(settings['assetdir']):
|
|
remove(asset['uri'])
|
|
except OSError:
|
|
pass
|
|
assets_helper.delete(conn, asset_id)
|
|
return '', 204 # return an OK with no content
|
|
|
|
|
|
class AssetsV1_2(Resource):
|
|
method_decorators = [authorized]
|
|
|
|
@swagger.doc({
|
|
'responses': {
|
|
'200': {
|
|
'description': 'List of assets',
|
|
'schema': {
|
|
'type': 'array',
|
|
'items': AssetModel
|
|
}
|
|
}
|
|
}
|
|
})
|
|
def get(self):
|
|
with db.conn(settings['database']) as conn:
|
|
return assets_helper.read(conn)
|
|
|
|
@api_response
|
|
@swagger.doc({
|
|
'parameters': [
|
|
{
|
|
'in': 'body',
|
|
'name': 'model',
|
|
'description': 'Adds an asset',
|
|
'schema': AssetRequestModel,
|
|
'required': True
|
|
}
|
|
],
|
|
'responses': {
|
|
'201': {
|
|
'description': 'Asset created',
|
|
'schema': AssetModel
|
|
}
|
|
}
|
|
})
|
|
def post(self):
|
|
request_environ = Request(request.environ)
|
|
asset = prepare_asset_v1_2(request_environ, unique_name=True)
|
|
if not asset['skip_asset_check'] and url_fails(asset['uri']):
|
|
raise Exception("Could not retrieve file. Check the asset URL.")
|
|
with db.conn(settings['database']) as conn:
|
|
assets = assets_helper.read(conn)
|
|
ids_of_active_assets = [x['asset_id'] for x in assets if x['is_active']]
|
|
|
|
asset = assets_helper.create(conn, asset)
|
|
|
|
if asset['is_active']:
|
|
ids_of_active_assets.insert(asset['play_order'], asset['asset_id'])
|
|
assets_helper.save_ordering(conn, ids_of_active_assets)
|
|
return assets_helper.read(conn, asset['asset_id']), 201
|
|
|
|
|
|
class AssetV1_2(Resource):
|
|
method_decorators = [api_response, authorized]
|
|
|
|
@swagger.doc({
|
|
'parameters': [
|
|
{
|
|
'name': 'asset_id',
|
|
'type': 'string',
|
|
'in': 'path',
|
|
'description': 'id of an asset'
|
|
}
|
|
],
|
|
'responses': {
|
|
'200': {
|
|
'description': 'Asset',
|
|
'schema': AssetModel
|
|
}
|
|
}
|
|
})
|
|
def get(self, asset_id):
|
|
with db.conn(settings['database']) as conn:
|
|
return assets_helper.read(conn, asset_id)
|
|
|
|
@swagger.doc({
|
|
'parameters': [
|
|
{
|
|
'name': 'asset_id',
|
|
'type': 'string',
|
|
'in': 'path',
|
|
'description': 'ID of an asset',
|
|
'required': True
|
|
},
|
|
{
|
|
'in': 'body',
|
|
'name': 'properties',
|
|
'description': 'Properties of an asset',
|
|
'schema': AssetPropertiesModel,
|
|
'required': True
|
|
}
|
|
],
|
|
'responses': {
|
|
'200': {
|
|
'description': 'Asset updated',
|
|
'schema': AssetModel
|
|
}
|
|
}
|
|
})
|
|
def patch(self, asset_id):
|
|
data = json.loads(request.data)
|
|
with db.conn(settings['database']) as conn:
|
|
|
|
asset = assets_helper.read(conn, asset_id)
|
|
if not asset:
|
|
raise Exception('Asset not found.')
|
|
update_asset(asset, data)
|
|
|
|
assets = assets_helper.read(conn)
|
|
ids_of_active_assets = [x['asset_id'] for x in assets if x['is_active']]
|
|
|
|
asset = assets_helper.update(conn, asset_id, asset)
|
|
|
|
try:
|
|
ids_of_active_assets.remove(asset['asset_id'])
|
|
except ValueError:
|
|
pass
|
|
if asset['is_active']:
|
|
ids_of_active_assets.insert(asset['play_order'], asset['asset_id'])
|
|
|
|
assets_helper.save_ordering(conn, ids_of_active_assets)
|
|
return assets_helper.read(conn, asset_id)
|
|
|
|
@swagger.doc({
|
|
'parameters': [
|
|
{
|
|
'name': 'asset_id',
|
|
'type': 'string',
|
|
'in': 'path',
|
|
'description': 'id of an asset',
|
|
'required': True
|
|
},
|
|
{
|
|
'in': 'body',
|
|
'name': 'model',
|
|
'description': 'Adds an asset',
|
|
'schema': AssetRequestModel,
|
|
'required': True
|
|
}
|
|
],
|
|
'responses': {
|
|
'200': {
|
|
'description': 'Asset updated',
|
|
'schema': AssetModel
|
|
}
|
|
}
|
|
})
|
|
def put(self, asset_id):
|
|
asset = prepare_asset_v1_2(request, asset_id)
|
|
with db.conn(settings['database']) as conn:
|
|
assets = assets_helper.read(conn)
|
|
ids_of_active_assets = [x['asset_id'] for x in assets if x['is_active']]
|
|
|
|
asset = assets_helper.update(conn, asset_id, asset)
|
|
|
|
try:
|
|
ids_of_active_assets.remove(asset['asset_id'])
|
|
except ValueError:
|
|
pass
|
|
if asset['is_active']:
|
|
ids_of_active_assets.insert(asset['play_order'], asset['asset_id'])
|
|
|
|
assets_helper.save_ordering(conn, ids_of_active_assets)
|
|
return assets_helper.read(conn, asset_id)
|
|
|
|
@swagger.doc({
|
|
'parameters': [
|
|
{
|
|
'name': 'asset_id',
|
|
'type': 'string',
|
|
'in': 'path',
|
|
'description': 'id of an asset',
|
|
'required': True
|
|
|
|
},
|
|
],
|
|
'responses': {
|
|
'204': {
|
|
'description': 'Deleted'
|
|
}
|
|
}
|
|
})
|
|
def delete(self, asset_id):
|
|
with db.conn(settings['database']) as conn:
|
|
asset = assets_helper.read(conn, asset_id)
|
|
try:
|
|
if asset['uri'].startswith(settings['assetdir']):
|
|
remove(asset['uri'])
|
|
except OSError:
|
|
pass
|
|
assets_helper.delete(conn, asset_id)
|
|
return '', 204 # return an OK with no content
|
|
|
|
|
|
class FileAsset(Resource):
|
|
method_decorators = [api_response, authorized]
|
|
|
|
@swagger.doc({
|
|
'parameters': [
|
|
{
|
|
'name': 'file_upload',
|
|
'type': 'file',
|
|
'in': 'formData',
|
|
'description': 'File to be sent'
|
|
}
|
|
],
|
|
'responses': {
|
|
'200': {
|
|
'description': 'File path',
|
|
'schema': {
|
|
'type': 'string'
|
|
}
|
|
}
|
|
}
|
|
})
|
|
def post(self):
|
|
req = Request(request.environ)
|
|
file_upload = req.files.get('file_upload')
|
|
filename = file_upload.filename.encode('utf-8')
|
|
file_type = guess_type(filename)[0]
|
|
|
|
if not file_type:
|
|
raise Exception("Invalid file type.")
|
|
|
|
if file_type.split('/')[0] not in ['image', 'video']:
|
|
raise Exception("Invalid file type.")
|
|
|
|
file_path = path.join(settings['assetdir'], uuid.uuid5(uuid.NAMESPACE_URL, filename).hex) + ".tmp"
|
|
|
|
if 'Content-Range' in request.headers:
|
|
range_str = request.headers['Content-Range']
|
|
start_bytes = int(range_str.split(' ')[1].split('-')[0])
|
|
with open(file_path, 'a') as f:
|
|
f.seek(start_bytes)
|
|
f.write(file_upload.read())
|
|
else:
|
|
file_upload.save(file_path)
|
|
|
|
return {'uri': file_path, 'ext': guess_extension(file_type)}
|
|
|
|
|
|
class PlaylistOrder(Resource):
|
|
method_decorators = [api_response, authorized]
|
|
|
|
@swagger.doc({
|
|
'parameters': [
|
|
{
|
|
'name': 'ids',
|
|
'in': 'formData',
|
|
'type': 'string',
|
|
'description':
|
|
'''
|
|
Content-Type: application/x-www-form-urlencoded
|
|
ids: "793406aa1fd34b85aa82614004c0e63a,1c5cfa719d1f4a9abae16c983a18903b,9c41068f3b7e452baf4dc3f9b7906595"
|
|
comma separated ids
|
|
'''
|
|
},
|
|
],
|
|
'responses': {
|
|
'204': {
|
|
'description': 'Sorted'
|
|
}
|
|
}
|
|
})
|
|
def post(self):
|
|
with db.conn(settings['database']) as conn:
|
|
assets_helper.save_ordering(conn, request.form.get('ids', '').split(','))
|
|
|
|
|
|
class Backup(Resource):
|
|
method_decorators = [api_response, authorized]
|
|
|
|
@swagger.doc({
|
|
'responses': {
|
|
'200': {
|
|
'description': 'Backup filename',
|
|
'schema': {
|
|
'type': 'string'
|
|
}
|
|
}
|
|
}
|
|
})
|
|
def post(self):
|
|
filename = backup_helper.create_backup(name=settings['player_name'])
|
|
return filename, 201
|
|
|
|
|
|
class Recover(Resource):
|
|
method_decorators = [api_response, authorized]
|
|
|
|
@swagger.doc({
|
|
'parameters': [
|
|
{
|
|
'name': 'backup_upload',
|
|
'type': 'file',
|
|
'in': 'formData'
|
|
}
|
|
],
|
|
'responses': {
|
|
'200': {
|
|
'description': 'Recovery successful'
|
|
}
|
|
}
|
|
})
|
|
def post(self):
|
|
publisher = ZmqPublisher.get_instance()
|
|
req = Request(request.environ)
|
|
file_upload = (req.files['backup_upload'])
|
|
filename = file_upload.filename
|
|
|
|
if guess_type(filename)[0] != 'application/x-tar':
|
|
raise Exception("Incorrect file extension.")
|
|
try:
|
|
publisher.send_to_viewer('stop')
|
|
location = path.join("static", filename)
|
|
file_upload.save(location)
|
|
backup_helper.recover(location)
|
|
return "Recovery successful."
|
|
finally:
|
|
publisher.send_to_viewer('play')
|
|
|
|
|
|
class ResetWifiConfig(Resource):
|
|
method_decorators = [api_response, authorized]
|
|
|
|
@swagger.doc({
|
|
'responses': {
|
|
'204': {
|
|
'description': 'Deleted'
|
|
}
|
|
}
|
|
})
|
|
def get(self):
|
|
home = getenv('HOME')
|
|
file_path = path.join(home, '.screenly/initialized')
|
|
|
|
if path.isfile(file_path):
|
|
remove(file_path)
|
|
|
|
bus = pydbus.SystemBus()
|
|
|
|
pattern_include = re.compile("wlan*")
|
|
pattern_exclude = re.compile("ScreenlyOSE-*")
|
|
|
|
wireless_connections = get_active_connections(bus)
|
|
|
|
if wireless_connections is not None:
|
|
device_uuid = None
|
|
|
|
wireless_connections = filter(
|
|
lambda c: not pattern_exclude.search(str(c['Id'])),
|
|
filter(
|
|
lambda c: pattern_include.search(str(c['Devices'])),
|
|
wireless_connections
|
|
)
|
|
)
|
|
|
|
if len(wireless_connections) > 0:
|
|
device_uuid = wireless_connections[0].get('Uuid')
|
|
|
|
if not device_uuid:
|
|
raise Exception('The device has no active connection.')
|
|
|
|
remove_connection(bus, device_uuid)
|
|
|
|
return '', 204
|
|
|
|
|
|
class GenerateUsbAssetsKey(Resource):
|
|
method_decorators = [api_response, authorized]
|
|
|
|
@swagger.doc({
|
|
'responses': {
|
|
'200': {
|
|
'description': 'Usb assets key generated',
|
|
'schema': {
|
|
'type': 'string'
|
|
}
|
|
}
|
|
}
|
|
})
|
|
def get(self):
|
|
settings['usb_assets_key'] = generate_perfect_paper_password(20, False)
|
|
settings.save()
|
|
|
|
return settings['usb_assets_key']
|
|
|
|
|
|
class UpgradeScreenly(Resource):
|
|
method_decorators = [api_response, authorized]
|
|
|
|
@swagger.doc({
|
|
'responses': {
|
|
'200': {
|
|
'description': 'Upgrade system'
|
|
}
|
|
}
|
|
})
|
|
def post(self):
|
|
for task in celery.control.inspect(timeout=2.0).active().get('worker@screenly'):
|
|
if task.get('type') == 'server.upgrade_screenly':
|
|
return jsonify({'id': task.get('id')})
|
|
branch = request.form.get('branch')
|
|
manage_network = request.form.get('manage_network')
|
|
system_upgrade = request.form.get('system_upgrade')
|
|
task = upgrade_screenly.apply_async(args=(branch, manage_network, system_upgrade))
|
|
return jsonify({'id': task.id})
|
|
|
|
|
|
@app.route('/upgrade_status/<task_id>')
|
|
def upgrade_screenly_status(task_id):
|
|
status_code = 200
|
|
task = upgrade_screenly.AsyncResult(task_id)
|
|
if task.state == 'PENDING':
|
|
response = {
|
|
'state': task.state,
|
|
'status': ''
|
|
}
|
|
status_code = 202
|
|
elif task.state == 'PROGRESS':
|
|
response = {
|
|
'state': task.state,
|
|
'status': task.info.get('status', '')
|
|
}
|
|
status_code = 202
|
|
elif task.state != 'FAILURE':
|
|
response = {
|
|
'state': task.state,
|
|
'status': task.info.get('status', '')
|
|
}
|
|
else:
|
|
response = {
|
|
'state': task.state,
|
|
'status': str(task.info)
|
|
}
|
|
return jsonify(response), status_code
|
|
|
|
|
|
class RebootScreenly(Resource):
|
|
method_decorators = [api_response, authorized]
|
|
|
|
@swagger.doc({
|
|
'responses': {
|
|
'200': {
|
|
'description': 'Reboot system'
|
|
}
|
|
}
|
|
})
|
|
def post(self):
|
|
reboot_screenly.apply_async()
|
|
return '', 200
|
|
|
|
|
|
class ShutdownScreenly(Resource):
|
|
method_decorators = [api_response, authorized]
|
|
|
|
@swagger.doc({
|
|
'responses': {
|
|
'200': {
|
|
'description': 'Shutdown system'
|
|
}
|
|
}
|
|
})
|
|
def post(self):
|
|
shutdown_screenly.apply_async()
|
|
return '', 200
|
|
|
|
|
|
class Info(Resource):
|
|
method_decorators = [api_response, authorized]
|
|
|
|
def get(self):
|
|
viewlog = "Not yet implemented"
|
|
|
|
# Calculate disk space
|
|
slash = statvfs("/")
|
|
free_space = size(slash.f_bavail * slash.f_frsize)
|
|
display_power = r.get('display_power')
|
|
|
|
return {
|
|
'viewlog': viewlog,
|
|
'loadavg': diagnostics.get_load_avg()['15 min'],
|
|
'free_space': free_space,
|
|
'display_info': diagnostics.get_monitor_status(),
|
|
'display_power': display_power,
|
|
'up_to_date': is_up_to_date()
|
|
}
|
|
|
|
|
|
class AssetsControl(Resource):
|
|
method_decorators = [api_response, authorized]
|
|
|
|
@swagger.doc({
|
|
'parameters': [
|
|
{
|
|
'name': 'command',
|
|
'type': 'string',
|
|
'in': 'path',
|
|
'description':
|
|
'''
|
|
Control commands:
|
|
next - show next asset
|
|
previous - show previous asset
|
|
asset&asset_id - show asset with `asset_id` id
|
|
'''
|
|
}
|
|
],
|
|
'responses': {
|
|
'200': {
|
|
'description': 'Asset switched'
|
|
}
|
|
}
|
|
})
|
|
def get(self, command):
|
|
publisher = ZmqPublisher.get_instance()
|
|
publisher.send_to_viewer(command)
|
|
return "Asset switched"
|
|
|
|
|
|
class AssetContent(Resource):
|
|
method_decorators = [api_response, authorized]
|
|
|
|
@swagger.doc({
|
|
'parameters': [
|
|
{
|
|
'name': 'asset_id',
|
|
'type': 'string',
|
|
'in': 'path',
|
|
'description': 'id of an asset'
|
|
}
|
|
],
|
|
'responses': {
|
|
'200': {
|
|
'description':
|
|
'''
|
|
The content of the asset.
|
|
|
|
'type' can either be 'file' or 'url'.
|
|
|
|
In case of a file, the fields 'mimetype', 'filename', and 'content' will be present.
|
|
In case of a URL, the field 'url' will be present.
|
|
''',
|
|
'schema': AssetContentModel
|
|
}
|
|
}
|
|
})
|
|
def get(self, asset_id):
|
|
with db.conn(settings['database']) as conn:
|
|
asset = assets_helper.read(conn, asset_id)
|
|
|
|
if isinstance(asset, list):
|
|
raise Exception('Invalid asset ID provided')
|
|
|
|
if path.isfile(asset['uri']):
|
|
filename = asset['name']
|
|
|
|
with open(asset['uri'], 'rb') as f:
|
|
content = f.read()
|
|
|
|
mimetype = guess_type(filename)[0]
|
|
if not mimetype:
|
|
mimetype = 'application/octet-stream'
|
|
|
|
result = {
|
|
'type': 'file',
|
|
'filename': filename,
|
|
'content': b64encode(content),
|
|
'mimetype': mimetype
|
|
}
|
|
else:
|
|
result = {
|
|
'type': 'url',
|
|
'url': asset['uri']
|
|
}
|
|
|
|
return result
|
|
|
|
|
|
class ViewerCurrentAsset(Resource):
|
|
method_decorators = [api_response, authorized]
|
|
|
|
@swagger.doc({
|
|
'responses': {
|
|
'200': {
|
|
'description': 'Currently displayed asset in viewer',
|
|
'schema': AssetModel
|
|
}
|
|
}
|
|
})
|
|
def get(self):
|
|
collector = ZmqCollector.get_instance()
|
|
|
|
publisher = ZmqPublisher.get_instance()
|
|
publisher.send_to_viewer('current_asset_id')
|
|
|
|
collector_result = collector.recv_json(2000)
|
|
current_asset_id = collector_result.get('current_asset_id')
|
|
|
|
if not current_asset_id:
|
|
return []
|
|
|
|
with db.conn(settings['database']) as conn:
|
|
return assets_helper.read(conn, current_asset_id)
|
|
|
|
|
|
api.add_resource(Assets, '/api/v1/assets')
|
|
api.add_resource(Asset, '/api/v1/assets/<asset_id>')
|
|
api.add_resource(AssetsV1_1, '/api/v1.1/assets')
|
|
api.add_resource(AssetV1_1, '/api/v1.1/assets/<asset_id>')
|
|
api.add_resource(AssetsV1_2, '/api/v1.2/assets')
|
|
api.add_resource(AssetV1_2, '/api/v1.2/assets/<asset_id>')
|
|
api.add_resource(AssetContent, '/api/v1/assets/<asset_id>/content')
|
|
api.add_resource(FileAsset, '/api/v1/file_asset')
|
|
api.add_resource(PlaylistOrder, '/api/v1/assets/order')
|
|
api.add_resource(Backup, '/api/v1/backup')
|
|
api.add_resource(Recover, '/api/v1/recover')
|
|
api.add_resource(AssetsControl, '/api/v1/assets/control/<command>')
|
|
api.add_resource(Info, '/api/v1/info')
|
|
api.add_resource(ResetWifiConfig, '/api/v1/reset_wifi')
|
|
api.add_resource(GenerateUsbAssetsKey, '/api/v1/generate_usb_assets_key')
|
|
api.add_resource(UpgradeScreenly, '/api/v1/upgrade_screenly')
|
|
api.add_resource(RebootScreenly, '/api/v1/reboot_screenly')
|
|
api.add_resource(ShutdownScreenly, '/api/v1/shutdown_screenly')
|
|
api.add_resource(ViewerCurrentAsset, '/api/v1/viewer_current_asset')
|
|
|
|
try:
|
|
my_ip = get_node_ip()
|
|
except Exception:
|
|
pass
|
|
else:
|
|
SWAGGER_URL = '/api/docs'
|
|
swagger_address = getenv("SWAGGER_HOST", my_ip)
|
|
|
|
if settings['use_ssl'] or is_demo_node:
|
|
API_URL = 'https://{}/api/swagger.json'.format(swagger_address)
|
|
elif LISTEN == '127.0.0.1' or swagger_address != my_ip:
|
|
API_URL = "http://{}/api/swagger.json".format(swagger_address)
|
|
else:
|
|
API_URL = "http://{}:{}/api/swagger.json".format(swagger_address, PORT)
|
|
|
|
swaggerui_blueprint = get_swaggerui_blueprint(
|
|
SWAGGER_URL,
|
|
API_URL,
|
|
config={
|
|
'app_name': "Screenly OSE API"
|
|
}
|
|
)
|
|
app.register_blueprint(swaggerui_blueprint, url_prefix=SWAGGER_URL)
|
|
|
|
|
|
################################
|
|
# Views
|
|
################################
|
|
|
|
|
|
@app.route('/')
|
|
@authorized
|
|
def viewIndex():
|
|
player_name = settings['player_name']
|
|
my_ip = urlparse(request.host_url).hostname
|
|
is_demo = is_demo_node()
|
|
resin_uuid = getenv("RESIN_UUID", None)
|
|
|
|
ws_addresses = []
|
|
|
|
if settings['use_ssl']:
|
|
ws_addresses.append('wss://' + my_ip + '/ws/')
|
|
else:
|
|
ws_addresses.append('ws://' + my_ip + '/ws/')
|
|
|
|
if resin_uuid:
|
|
ws_addresses.append('wss://{}.resindevice.io/ws/'.format(resin_uuid))
|
|
|
|
return template(
|
|
'index.html',
|
|
ws_addresses=ws_addresses,
|
|
player_name=player_name,
|
|
is_demo=is_demo,
|
|
is_balena=is_balena_app(),
|
|
)
|
|
|
|
|
|
@app.route('/settings', methods=["GET", "POST"])
|
|
@authorized
|
|
def settings_page():
|
|
context = {'flash': None}
|
|
|
|
if request.method == "POST":
|
|
try:
|
|
# put some request variables in local variables to make easier to read
|
|
current_pass = request.form.get('current-password', '')
|
|
auth_backend = request.form.get('auth_backend', '')
|
|
|
|
if auth_backend != settings['auth_backend'] and settings['auth_backend']:
|
|
if not current_pass:
|
|
raise ValueError("Must supply current password to change authentication method")
|
|
if not settings.auth.check_password(current_pass):
|
|
raise ValueError("Incorrect current password.")
|
|
|
|
prev_auth_backend = settings['auth_backend']
|
|
if not current_pass and prev_auth_backend:
|
|
current_pass_correct = None
|
|
else:
|
|
current_pass_correct = settings.auth_backends[prev_auth_backend].check_password(current_pass)
|
|
next_auth_backend = settings.auth_backends[auth_backend]
|
|
next_auth_backend.update_settings(current_pass_correct)
|
|
settings['auth_backend'] = auth_backend
|
|
|
|
for field, default in CONFIGURABLE_SETTINGS.items():
|
|
value = request.form.get(field, default)
|
|
|
|
if not value and field in ['default_duration', 'default_streaming_duration']:
|
|
value = str(0)
|
|
if isinstance(default, bool):
|
|
value = value == 'on'
|
|
|
|
if field == 'default_assets' and settings[field] != value:
|
|
if value:
|
|
add_default_assets()
|
|
else:
|
|
remove_default_assets()
|
|
|
|
settings[field] = value
|
|
|
|
settings.save()
|
|
publisher = ZmqPublisher.get_instance()
|
|
publisher.send_to_viewer('reload')
|
|
context['flash'] = {'class': "success", 'message': "Settings were successfully saved."}
|
|
except ValueError as e:
|
|
context['flash'] = {'class': "danger", 'message': e}
|
|
except IOError as e:
|
|
context['flash'] = {'class': "danger", 'message': e}
|
|
except OSError as e:
|
|
context['flash'] = {'class': "danger", 'message': e}
|
|
else:
|
|
settings.load()
|
|
for field, default in DEFAULTS['viewer'].items():
|
|
if field == 'usb_assets_key':
|
|
if not settings[field]:
|
|
settings[field] = generate_perfect_paper_password(20, False)
|
|
settings.save()
|
|
context[field] = settings[field]
|
|
|
|
auth_backends = []
|
|
for backend in settings.auth_backends_list:
|
|
if backend.template:
|
|
html, ctx = backend.template
|
|
context.update(ctx)
|
|
else:
|
|
html = None
|
|
auth_backends.append({
|
|
'name': backend.name,
|
|
'text': backend.display_name,
|
|
'template': html,
|
|
'selected': 'selected' if settings['auth_backend'] == backend.name else ''
|
|
})
|
|
|
|
context.update({
|
|
'user': settings['user'],
|
|
'need_current_password': bool(settings['auth_backend']),
|
|
'is_balena': is_balena_app(),
|
|
'is_docker': is_docker(),
|
|
'auth_backend': settings['auth_backend'],
|
|
'auth_backends': auth_backends
|
|
})
|
|
|
|
return template('settings.html', **context)
|
|
|
|
|
|
@app.route('/system-info')
|
|
@authorized
|
|
def system_info():
|
|
viewlog = ["Yet to be implemented"]
|
|
|
|
loadavg = diagnostics.get_load_avg()['15 min']
|
|
display_info = diagnostics.get_monitor_status()
|
|
display_power = r.get('display_power')
|
|
|
|
# Calculate disk space
|
|
slash = statvfs("/")
|
|
free_space = size(slash.f_bavail * slash.f_frsize)
|
|
|
|
# Memory
|
|
virtual_memory = psutil.virtual_memory()
|
|
memory = {
|
|
'total': virtual_memory.total >> 20,
|
|
'used': virtual_memory.used >> 20,
|
|
'free': virtual_memory.free >> 20,
|
|
'shared': virtual_memory.shared >> 20,
|
|
'buff': virtual_memory.buffers >> 20,
|
|
'available': virtual_memory.available >> 20
|
|
}
|
|
|
|
# Get uptime
|
|
system_uptime = timedelta(seconds=diagnostics.get_uptime())
|
|
|
|
# Player name for title
|
|
player_name = settings['player_name']
|
|
|
|
raspberry_pi_model = raspberry_pi_helper.parse_cpu_info().get('model', "Unknown")
|
|
|
|
screenly_version = '{}@{}'.format(
|
|
diagnostics.get_git_branch(),
|
|
diagnostics.get_git_short_hash()
|
|
)
|
|
|
|
return template(
|
|
'system-info.html',
|
|
player_name=player_name,
|
|
viewlog=viewlog,
|
|
loadavg=loadavg,
|
|
free_space=free_space,
|
|
uptime=system_uptime,
|
|
memory=memory,
|
|
display_info=display_info,
|
|
display_power=display_power,
|
|
raspberry_pi_model=raspberry_pi_model,
|
|
screenly_version=screenly_version,
|
|
mac_address=get_node_mac_address(),
|
|
is_balena=is_balena_app(),
|
|
)
|
|
|
|
|
|
@app.route('/integrations')
|
|
@authorized
|
|
def integrations():
|
|
|
|
context = {
|
|
'player_name': settings['player_name'],
|
|
'is_balena': is_balena_app(),
|
|
}
|
|
|
|
if context['is_balena']:
|
|
context['balena_device_id'] = getenv('BALENA_DEVICE_UUID')
|
|
context['balena_app_id'] = getenv('BALENA_APP_ID')
|
|
context['balena_app_name'] = getenv('BALENA_APP_NAME')
|
|
context['balena_supervisor_version'] = getenv('BALENA_SUPERVISOR_VERSION')
|
|
context['balena_host_os_version'] = getenv('BALENA_HOST_OS_VERSION')
|
|
context['balena_device_name_at_init'] = getenv('BALENA_DEVICE_NAME_AT_INIT')
|
|
|
|
return template('integrations.html', **context)
|
|
|
|
|
|
@app.route('/splash-page')
|
|
def splash_page():
|
|
my_ip = get_node_ip().split()
|
|
return template('splash-page.html', my_ip=my_ip)
|
|
|
|
|
|
@app.errorhandler(403)
|
|
def mistake403(code):
|
|
return 'The parameter you passed has the wrong format!'
|
|
|
|
|
|
@app.errorhandler(404)
|
|
def mistake404(code):
|
|
return 'Sorry, this page does not exist!'
|
|
|
|
|
|
################################
|
|
# Static
|
|
################################
|
|
|
|
|
|
@app.context_processor
|
|
def override_url_for():
|
|
return dict(url_for=dated_url_for)
|
|
|
|
|
|
def dated_url_for(endpoint, **values):
|
|
if endpoint == 'static':
|
|
filename = values.get('filename', None)
|
|
if filename:
|
|
file_path = path.join(app.root_path,
|
|
endpoint, filename)
|
|
if path.isfile(file_path):
|
|
values['q'] = int(stat(file_path).st_mtime)
|
|
return url_for(endpoint, **values)
|
|
|
|
|
|
@app.route('/static_with_mime/<string:path>')
|
|
@authorized
|
|
def static_with_mime(path):
|
|
mimetype = request.args['mime'] if 'mime' in request.args else 'auto'
|
|
return send_from_directory(directory='static', filename=path, mimetype=mimetype)
|
|
|
|
|
|
@app.before_first_request
|
|
def main():
|
|
# Make sure the asset folder exist. If not, create it
|
|
if not path.isdir(settings['assetdir']):
|
|
mkdir(settings['assetdir'])
|
|
# Create config dir if it doesn't exist
|
|
if not path.isdir(settings.get_configdir()):
|
|
makedirs(settings.get_configdir())
|
|
|
|
with db.conn(settings['database']) as conn:
|
|
with db.cursor(conn) as cursor:
|
|
cursor.execute(queries.exists_table)
|
|
if cursor.fetchone() is None:
|
|
cursor.execute(assets_helper.create_assets_table)
|
|
|
|
|
|
def is_development():
|
|
return getenv('FLASK_ENV', '') == 'development'
|
|
|
|
|
|
if __name__ == "__main__" and not is_development():
|
|
config = {
|
|
'bind': '{}:{}'.format(LISTEN, PORT),
|
|
'threads': 2,
|
|
'timeout': 20
|
|
}
|
|
|
|
class GunicornApplication(Application):
|
|
def init(self, parser, opts, args):
|
|
return config
|
|
|
|
def load(self):
|
|
return app
|
|
|
|
GunicornApplication().run()
|