From 322d9992fc81240a0a088e7eb1b1203db70cf20b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=2E=20Fernando=20S=C3=A1nchez?=
Date: Mon, 8 Jun 2015 15:27:55 +0200
Subject: [PATCH] Added WebUI
---
Adafruit_Video_Looper/directory.py | 9 +-
Adafruit_Video_Looper/omxplayer.py | 1 +
.../templates/admin/index.html | 20 ++
Adafruit_Video_Looper/templates/index.html | 7 +
.../templates/my_master.html | 19 ++
.../templates/security/_macros.html | 16 ++
.../templates/security/_menu.html | 15 ++
.../templates/security/_messages.html | 9 +
.../templates/security/login_user.html | 18 ++
.../templates/security/register_user.html | 19 ++
Adafruit_Video_Looper/usb_drive.py | 3 +-
Adafruit_Video_Looper/video_looper.py | 32 +--
Adafruit_Video_Looper/webui.py | 203 ++++++++++++++++++
setup.py | 10 +-
video_looper.conf | 6 +
video_looper.ini | 34 ++-
16 files changed, 397 insertions(+), 24 deletions(-)
create mode 100644 Adafruit_Video_Looper/templates/admin/index.html
create mode 100644 Adafruit_Video_Looper/templates/index.html
create mode 100644 Adafruit_Video_Looper/templates/my_master.html
create mode 100644 Adafruit_Video_Looper/templates/security/_macros.html
create mode 100644 Adafruit_Video_Looper/templates/security/_menu.html
create mode 100644 Adafruit_Video_Looper/templates/security/_messages.html
create mode 100644 Adafruit_Video_Looper/templates/security/login_user.html
create mode 100644 Adafruit_Video_Looper/templates/security/register_user.html
create mode 100644 Adafruit_Video_Looper/webui.py
diff --git a/Adafruit_Video_Looper/directory.py b/Adafruit_Video_Looper/directory.py
index eaca2dd..e67f145 100644
--- a/Adafruit_Video_Looper/directory.py
+++ b/Adafruit_Video_Looper/directory.py
@@ -1,6 +1,9 @@
# Copyright 2015 Adafruit Industries.
# Author: Tony DiCola
# License: GNU GPLv2, see LICENSE.txt
+
+import os.path
+
class DirectoryReader(object):
def __init__(self, config):
@@ -8,6 +11,7 @@ class DirectoryReader(object):
directory on disk.
"""
self._load_config(config)
+ self._mtime = 0
def _load_config(self, config):
self._path = config.get('directory', 'path')
@@ -23,7 +27,10 @@ class DirectoryReader(object):
# true if new files are added/removed from the directory. This is
# called in a tight loop of the main program so it needs to be fast and
# not resource intensive.
- return False
+ mtime = os.path.getmtime(self._path)
+ changed = mtime and mtime > self._mtime
+ self._mtime = mtime
+ return changed
def idle_message(self):
"""Return a message to display when idle and no files are found."""
diff --git a/Adafruit_Video_Looper/omxplayer.py b/Adafruit_Video_Looper/omxplayer.py
index 83ea9a5..a52b690 100644
--- a/Adafruit_Video_Looper/omxplayer.py
+++ b/Adafruit_Video_Looper/omxplayer.py
@@ -38,6 +38,7 @@ class OMXPlayer(object):
args.extend(['--vol', str(vol)])
if loop:
args.append('--loop') # Add loop parameter if necessary.
+ args.append
args.append(movie) # Add movie file path.
# Run omxplayer process and direct standard output to /dev/null.
self._process = subprocess.Popen(args,
diff --git a/Adafruit_Video_Looper/templates/admin/index.html b/Adafruit_Video_Looper/templates/admin/index.html
new file mode 100644
index 0000000..2677ef1
--- /dev/null
+++ b/Adafruit_Video_Looper/templates/admin/index.html
@@ -0,0 +1,20 @@
+{% extends 'admin/master.html' %}
+{% block body %}
+{{ super() }}
+
+
+
+
Flask-Admin example
+
+ WebUI
+
+ {% if not current_user.is_authenticated() %}
+
+ login register
+
+ {% endif %}
+
+
+
Back
+
+{% endblock body %}
diff --git a/Adafruit_Video_Looper/templates/index.html b/Adafruit_Video_Looper/templates/index.html
new file mode 100644
index 0000000..09d17c6
--- /dev/null
+++ b/Adafruit_Video_Looper/templates/index.html
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/Adafruit_Video_Looper/templates/my_master.html b/Adafruit_Video_Looper/templates/my_master.html
new file mode 100644
index 0000000..5ed7b8c
--- /dev/null
+++ b/Adafruit_Video_Looper/templates/my_master.html
@@ -0,0 +1,19 @@
+{% extends 'admin/base.html' %}
+
+{% block access_control %}
+{% if current_user.is_authenticated() %}
+
+{% endif %}
+{% endblock %}
\ No newline at end of file
diff --git a/Adafruit_Video_Looper/templates/security/_macros.html b/Adafruit_Video_Looper/templates/security/_macros.html
new file mode 100644
index 0000000..9122f85
--- /dev/null
+++ b/Adafruit_Video_Looper/templates/security/_macros.html
@@ -0,0 +1,16 @@
+{% macro render_field_with_errors(field) %}
+
+ {{ field.label }} {{ field(**kwargs)|safe }}
+ {% if field.errors %}
+
+ {% for error in field.errors %}
+ - {{ error }}
+ {% endfor %}
+
+{% endif %}
+
+{% endmacro %}
+
+{% macro render_field(field) %}
+{{ field(**kwargs)|safe }}
+{% endmacro %}
\ No newline at end of file
diff --git a/Adafruit_Video_Looper/templates/security/_menu.html b/Adafruit_Video_Looper/templates/security/_menu.html
new file mode 100644
index 0000000..9e251b7
--- /dev/null
+++ b/Adafruit_Video_Looper/templates/security/_menu.html
@@ -0,0 +1,15 @@
+{% if security.registerable or security.recoverable or security.confirmable %}
+Menu
+
+{% endif %}
diff --git a/Adafruit_Video_Looper/templates/security/_messages.html b/Adafruit_Video_Looper/templates/security/_messages.html
new file mode 100644
index 0000000..1589a8b
--- /dev/null
+++ b/Adafruit_Video_Looper/templates/security/_messages.html
@@ -0,0 +1,9 @@
+{%- with messages = get_flashed_messages(with_categories=true) -%}
+{% if messages %}
+
+ {% for category, message in messages %}
+ - {{ message }}
+ {% endfor %}
+
+{% endif %}
+{%- endwith %}
\ No newline at end of file
diff --git a/Adafruit_Video_Looper/templates/security/login_user.html b/Adafruit_Video_Looper/templates/security/login_user.html
new file mode 100644
index 0000000..6f6620e
--- /dev/null
+++ b/Adafruit_Video_Looper/templates/security/login_user.html
@@ -0,0 +1,18 @@
+{% extends 'admin/master.html' %}
+{% from "security/_macros.html" import render_field_with_errors, render_field %}
+{% include "security/_messages.html" %}
+{% block body %}
+{{ super() }}
+
+
Login
+
+ {% include "security/_menu.html" %}
+
+{% endblock body %}
\ No newline at end of file
diff --git a/Adafruit_Video_Looper/templates/security/register_user.html b/Adafruit_Video_Looper/templates/security/register_user.html
new file mode 100644
index 0000000..b957f90
--- /dev/null
+++ b/Adafruit_Video_Looper/templates/security/register_user.html
@@ -0,0 +1,19 @@
+{% extends 'admin/master.html' %}
+{% from "security/_macros.html" import render_field_with_errors, render_field %}
+{% include "security/_messages.html" %}
+{% block body %}
+{{ super() }}
+
+
Register
+
+ {% include "security/_menu.html" %}
+
+{% endblock body %}
\ No newline at end of file
diff --git a/Adafruit_Video_Looper/usb_drive.py b/Adafruit_Video_Looper/usb_drive.py
index 64bb2a4..ab05f48 100644
--- a/Adafruit_Video_Looper/usb_drive.py
+++ b/Adafruit_Video_Looper/usb_drive.py
@@ -1,7 +1,6 @@
# Copyright 2015 Adafruit Industries.
# Author: Tony DiCola
# License: GNU GPLv2, see LICENSE.txt
-import glob
from usb_drive_mounter import USBDriveMounter
@@ -28,7 +27,7 @@ class USBDriveReader(object):
mounted USB drives.
"""
self._mounter.mount_all()
- return glob.glob(self._mount_path + '*')
+ return [self._mount_path]
def is_changed(self):
"""Return true if the file search paths have changed, like when a new
diff --git a/Adafruit_Video_Looper/video_looper.py b/Adafruit_Video_Looper/video_looper.py
index 6a40741..de1cb0f 100644
--- a/Adafruit_Video_Looper/video_looper.py
+++ b/Adafruit_Video_Looper/video_looper.py
@@ -8,6 +8,7 @@ import re
import sys
import signal
import time
+import glob
import pygame
@@ -116,21 +117,19 @@ class VideoLooper(object):
# Skip paths that don't exist or are files.
if not os.path.exists(path) or not os.path.isdir(path):
continue
- # Ignore hidden files (useful when file loaded on usb
- # key from an OSX computer
- movies.extend(['{0}/{1}'.format(path.rstrip('/'), x) \
- for x in os.listdir(path) \
- if re.search('\.{0}$'.format(ex), x,
- flags=re.IGNORECASE) and \
- x[0] is not '.'])
- # Get the video volume from the file in the usb key
- sound_vol_file_path = '{0}/{1}'.format(path.rstrip('/'), self._sound_vol_file)
- if os.path.exists(sound_vol_file_path):
- with open(sound_vol_file_path, 'r') as sound_file:
- sound_vol_string = sound_file.readline()
- if self._is_number(sound_vol_string):
- self._sound_vol = int(float(sound_vol_string))
- # Create a playlist with the sorted list of movies.
+ for root, dirnames, filenames in os.walk(path):
+ # Ignore hidden files (useful when file loaded on usb
+ # key from an OSX computer
+ for file in glob.glob('{}/[!.]*.{}'.format(root, ex)):
+ movies.append(os.path.join(root, file))
+ # Get the video volume from the file in the usb key
+ sound_vol_file_path = '{0}/{1}'.format(root.rstrip('/'), self._sound_vol_file)
+ if os.path.exists(sound_vol_file_path):
+ with open(sound_vol_file_path, 'r') as sound_file:
+ sound_vol_string = sound_file.readline()
+ if self._is_number(sound_vol_string):
+ self._sound_vol = int(float(sound_vol_string))
+ # Create a playlist with the sorted list of movies.
return Playlist(sorted(movies), self._is_random)
def _blank_screen(self):
@@ -147,7 +146,7 @@ class VideoLooper(object):
font = self._small_font
return font.render(message, True, self._fgcolor, self._bgcolor)
- def _animate_countdown(self, playlist, seconds=10):
+ def _animate_countdown(self, playlist, seconds=3):
"""Print text with the number of loaded movies and a quick countdown
message if the on screen display is enabled.
"""
@@ -219,6 +218,7 @@ class VideoLooper(object):
# Check for changes in the file search path (like USB drives added)
# and rebuild the playlist.
if self._reader.is_changed():
+ self._print("CHANGED")
self._player.stop(3) # Up to 3 second delay waiting for old
# player to stop.
# Rebuild playlist and show countdown again (if OSD enabled).
diff --git a/Adafruit_Video_Looper/webui.py b/Adafruit_Video_Looper/webui.py
new file mode 100644
index 0000000..bbb6d23
--- /dev/null
+++ b/Adafruit_Video_Looper/webui.py
@@ -0,0 +1,203 @@
+import os
+import sys
+import ConfigParser
+import os.path as op
+from flask import Flask, url_for, redirect, render_template, request, abort
+from flask_sqlalchemy import SQLAlchemy
+from flask_security import Security, SQLAlchemyUserDatastore, \
+ UserMixin, RoleMixin, login_required, current_user
+from flask_security.utils import encrypt_password
+import flask_admin
+from flask_admin.contrib import sqla
+from flask_admin.contrib import fileadmin
+from flask_admin import helpers as admin_helpers
+from flask_ini import FlaskIni
+
+
+# Create Flask application
+app = Flask(__name__)
+db = SQLAlchemy(app)
+
+config_path = '/boot/video_looper.ini'
+if len(sys.argv) == 2:
+ config_path = sys.argv[1]
+with app.app_context():
+ app.iniconfig = FlaskIni()
+ app.iniconfig.read(config_path)
+# Build a sample db on the fly, if one does not exist yet.
+app_dir = os.path.realpath(os.path.dirname(__file__))
+database_path = os.path.join(app_dir, app.config['DATABASE_FILE'])
+if not os.path.exists(database_path):
+ build_sample_db()
+
+print app.config["SECURITY_POST_LOGIN_VIEW"]
+# Define models
+roles_users = db.Table(
+ 'roles_users',
+ db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
+ db.Column('role_id', db.Integer(), db.ForeignKey('role.id'))
+)
+
+
+class Role(db.Model, RoleMixin):
+ id = db.Column(db.Integer(), primary_key=True)
+ name = db.Column(db.String(80), unique=True)
+ description = db.Column(db.String(255))
+
+ def __repr__(self):
+ return self.name
+
+class User(db.Model, UserMixin):
+ id = db.Column(db.Integer, primary_key=True)
+ email = db.Column(db.String(255), unique=True)
+ password = db.Column(db.String(255))
+ active = db.Column(db.Boolean())
+ roles = db.relationship('Role', secondary=roles_users,
+ backref=db.backref('users', lazy='dynamic'))
+
+
+# Setup Flask-Security
+user_datastore = SQLAlchemyUserDatastore(db, User, Role)
+security = Security(app, user_datastore)
+
+
+# Create customized model view class
+class MyModelView(sqla.ModelView):
+
+ def is_accessible(self):
+ if not current_user.is_active() or not current_user.is_authenticated():
+ return False
+
+ if current_user.has_role('superuser'):
+ return True
+
+ return False
+
+ def _handle_view(self, name, **kwargs):
+ """
+ Override builtin _handle_view in order to redirect users when a view is not accessible.
+ """
+ if not self.is_accessible():
+ if current_user.is_authenticated():
+ # permission denied
+ abort(403)
+ else:
+ # login
+ return redirect(url_for('security.login', next=request.url))
+
+# Flask views
+@app.route('/')
+def index():
+ return render_template('index.html')
+
+# Create admin
+admin = flask_admin.Admin(app, 'Example: Auth', base_template='my_master.html')
+
+# Add model views
+admin.add_view(MyModelView(Role, db.session))
+admin.add_view(MyModelView(User, db.session))
+
+# define a context processor for merging flask-admin's template context into the
+# flask-security views.
+@security.context_processor
+def security_context_processor():
+ return dict(
+ admin_base_template=admin.base_template,
+ admin_view=admin.index_view,
+ h=admin_helpers,
+ )
+
+
+def build_sample_db():
+ """
+ Populate a small db with some example entries.
+ """
+
+ import string
+ import random
+
+ db.drop_all()
+ db.create_all()
+
+ with app.app_context():
+ user_role = Role(name='user')
+ super_user_role = Role(name='superuser')
+ db.session.add(user_role)
+ db.session.add(super_user_role)
+ db.session.commit()
+
+ test_user = user_datastore.create_user(
+ email='admin',
+ password=encrypt_password('admin'),
+ roles=[user_role, super_user_role]
+ )
+
+ emails = [
+ 'rpi',
+ ]
+
+ passwords = [
+ 'rpi'
+
+ ]
+
+ for i in range(len(emails)):
+ #tmp_email = first_names[i].lower() + "." + last_names[i].lower() + "@example.com"
+ #tmp_pass = ''.join(random.choice(string.ascii_lowercase + string.digits) for i in range(10))
+ user_datastore.create_user(
+ email=emails[i],
+ password=encrypt_password(passwords[i]),
+ roles=[user_role, ]
+ )
+ db.session.commit()
+ return
+
+
+class VideoAdmin(fileadmin.FileAdmin):
+ def __init__(self):
+ super(VideoAdmin, self).__init__(app.iniconfig.get("directory", "path"),
+ '/files/', name='Files')
+
+ def get_base_path(self):
+ return self.user_path
+
+ @property
+ def user_path(self):
+ if current_user.has_role('superuser'):
+ return self.base_dir
+ path = op.normpath('{}/{}'.format(app.iniconfig.get("directory", "path"),
+ current_user.email))
+ if not op.exists(path):
+ os.makedirs(path)
+ return path
+
+ @property
+ def base_dir(self):
+ return app.iniconfig.get("directory", "path")
+
+ def is_accessible_path(self, path):
+ return True
+
+ def update_base(self):
+ os.utime(self.base_dir, None)
+
+ def on_file_upload(self, *args, **kwargs):
+ self.update_base()
+
+ def on_edit_file(self, *args, **kwargs):
+ self.update_base()
+
+ def on_file_delete(self, *args, **kwargs):
+ self.update_base()
+
+ def on_rename(self, *args, **kwargs):
+ self.update_base()
+
+
+videoadmin = VideoAdmin()
+admin.add_view(videoadmin)
+
+if __name__ == '__main__':
+ app.run(host=app.config.get("HOST", "localhost"),
+ port=app.config.get("PORT", 5000),
+ debug=True)
diff --git a/setup.py b/setup.py
index a2fbaf2..c5e6bd3 100644
--- a/setup.py
+++ b/setup.py
@@ -3,11 +3,17 @@ use_setuptools()
from setuptools import setup, find_packages
setup(name = 'Adafruit_Video_Looper',
- version = '1.0.0',
+ version = '1.1.0',
author = 'Tony DiCola',
author_email = 'tdicola@adafruit.com',
description = 'Application to turn your Raspberry Pi into a dedicated looping video playback device, good for art installations, information displays, or just playing cat videos all day.',
license = 'GNU GPLv2',
url = 'https://github.com/adafruit/pi_video_looper',
- install_requires = ['pyudev'],
+ install_requires = ['pyudev',
+ 'Flask',
+ 'Flask-Admin',
+ 'Flask-Ini',
+ 'Flask-SQLAlchemy',
+ 'Flask-Security',
+ ],
packages = find_packages())
diff --git a/video_looper.conf b/video_looper.conf
index b769d4e..acc6aa7 100644
--- a/video_looper.conf
+++ b/video_looper.conf
@@ -5,3 +5,9 @@ command=python -u -m Adafruit_Video_Looper.video_looper
autostart=true
autorestart=unexpected
startsecs=5
+
+[program:video_looper_webui]
+command=python -u -m Adafruit_Video_Looper.webui
+autostart=true
+autorestart=unexpected
+startsecs=5
diff --git a/video_looper.ini b/video_looper.ini
index 9126237..9c16ad5 100644
--- a/video_looper.ini
+++ b/video_looper.ini
@@ -20,8 +20,8 @@ video_player = omxplayer
# directory option will search only a specified directory on the SD card for
# movie files. Note that you change the directory by modifying the setting in
# the [directory] section below. The default is usb_drive.
-file_reader = usb_drive
-#file_reader = directory
+#file_reader = usb_drive
+file_reader = directory
# The rest of the configuration for video looper below is optional and can be
# ignored.
@@ -54,7 +54,7 @@ console_output = true
[directory]
# The path to search for movies when using the directory file reader.
-path = /home/pi
+path = /home/pi/pi_video_looper/videos/
# USB drive file reader configuration follows.
[usb_drive]
@@ -98,3 +98,31 @@ extra_args = --no-osd --audio_fifo 0.01 --video_fifo 0.01
# List of supported file extensions. Must be comma separated and should not
# include the dot at the start of the extension.
extensions = h264
+
+[flask]
+host = 0.0.0.0
+# create dummy secrey key so we can use sessions
+secret_key = 123456790
+
+# create in-memory database
+database_file = sample_db.sqlite
+sqlalchemy_database_uri = sqlite:///sample_db.sqlite
+sqlalchemy_echo = true
+
+# flask-security config
+security_url_prefix = /admin
+security_password_hash = pbkdf2_sha512
+security_password_salt = atguohaelkiubahiughaergojaegj
+
+# flask-security urls, overridden because they don't put a / at the end
+security_login_url = /login/
+security_logout_url = /logout/
+security_register_url = /register/
+
+security_post_login_view = /admin/
+security_post_logout_view = /admin/
+security_post_register_view = /admin/
+
+# flask-security features
+security_registerable = true
+security_send_register_email = false