This commit is contained in:
Erhard 2017-11-14 22:18:06 +01:00
commit a18db979bb
9 changed files with 343 additions and 55 deletions

1
.gitignore vendored
View File

@ -8,3 +8,4 @@
# Project specific files # Project specific files
config.py config.py
metadata.sqlite*

View File

@ -1 +1,2 @@
path = "/home/pi/sounds" path = "/home/pi/sounds"
db = "metadata.sqlite"

18
schema.sql Normal file
View File

@ -0,0 +1,18 @@
CREATE TABLE IF NOT EXISTS tag (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE
);
CREATE TABLE IF NOT EXISTS button (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file TEXT,
checksum TEXT
);
CREATE TABLE IF NOT EXISTS button_tags (
fk_tag INTEGER,
fk_button INTEGER,
FOREIGN KEY(fk_tag) REFERENCES tag(id),
FOREIGN KEY(fk_button) REFERENCES button(id),
PRIMARY KEY(fk_tag, fk_button)
);

View File

@ -1,7 +1,9 @@
import os import os
import sys
import subprocess import subprocess
import sqlite3
from flask import Flask, render_template, request, redirect, url_for, send_from_directory from flask import Flask, render_template, request, redirect, url_for, send_from_directory, g
import config import config
@ -9,50 +11,138 @@ app = Flask(__name__)
app.jinja_env.trim_blocks = True app.jinja_env.trim_blocks = True
app.jinja_env.lstrip_blocks = True app.jinja_env.lstrip_blocks = True
processlist = [] # Credits: http://flask.pocoo.org/docs/0.12/patterns/sqlite3/
def getDB():
db = getattr(g, "_database", None)
if db is None:
if os.path.isabs(config.db):
dbPath = config.db
else:
dbPath = os.path.join(sys.path[0], config.db)
db = g._database = sqlite3.connect(dbPath)
db.row_factory = sqlite3.Row
return db
@app.teardown_appcontext
def closeDBConnection(exception):
db = getattr(g, "_database", None)
if db is not None:
db.close()
def queryDB(query, args=(), one=False):
cur = getDB().execute(query, args)
result = cur.fetchall()
cur.close()
return (result[0] if result else None) if one else result
@app.route("/") @app.route("/")
@app.route("/edit")
@app.route("/play/<sound>") @app.route("/play/<sound>")
@app.route("/say/", methods=["POST"]) @app.route("/say/", methods=["POST"])
@app.route("/say/<text>") @app.route("/say/<text>")
def index(sound=None, text=None, video=None): def index(sound=None, text=None, video=None):
sounds = [os.fsencode(file).decode() for file in os.listdir(config.path)] sounds = [os.fsencode(file).decode() for file in os.listdir(config.path)]
sounds = sorted(sounds) sounds = sorted(sounds)
if sound is not None and sound in sounds: tags = queryDB("SELECT name FROM tag ORDER BY name COLLATE NOCASE")
subprocess.Popen(["omxplayer", os.path.join(config.path, sound).encode("utf-8")], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
if text is None: if sound is not None and sound in sounds:
text = request.form.get("text") subprocess.Popen(["omxplayer", os.path.join(config.path, sound).encode("utf-8")], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
if text is not None: if text is None:
voice = request.form.get("voice", default="") text = request.form.get("text")
voice = voice if voice.strip() != "" else "DE"
speed = request.form.get("speed", default="")
speed = speed if speed.strip() != "" else "160"
pitch = request.form.get("pitch", default="")
pitch = pitch if pitch.strip() != "" else "50"
subprocess.Popen(["espeak", "-v", voice, "-s", speed, "-p", pitch, text.encode("utf-8")], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) if text is not None:
return redirect("/") voice = request.form.get("voice", default="")
voice = voice if voice.strip() != "" else "DE"
speed = request.form.get("speed", default="")
speed = speed if speed.strip() != "" else "160"
pitch = request.form.get("pitch", default="")
pitch = pitch if pitch.strip() != "" else "50"
video = request.args.get("video") subprocess.Popen(["espeak", "-v", voice, "-s", speed, "-p", pitch, text.encode("utf-8")], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
return redirect("/")
if video is not None: video = request.args.get("video")
if video[-4:] == ".mp3": if video is not None:
subprocess.Popen(["omxplayer", video], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) if video[-4:] == ".mp3":
else: subprocess.Popen(["omxplayer", video], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
url = subprocess.check_output(["youtube-dl", "-g", "-f", "mp4", video]).decode() else:
subprocess.Popen(["omxplayer", url.split("\n")[1]]) url = subprocess.check_output(["youtube-dl", "-g", "-f", "mp4", video]).decode()
subprocess.Popen(["omxplayer", "-b", url.split("\n")[0]]) subprocess.Popen(["omxplayer", url.split("\n")[1]])
subprocess.Popen(["omxplayer", "-b", url.split("\n")[0]])
killvideo = request.args.get("killvideo") killvideo = request.args.get("killvideo")
if killvideo is not None and killvideo in ["1", "true", "yes"]: if killvideo is not None and killvideo in ["1", "true", "yes"]:
subprocess.Popen(["pkill", "-f", "omxplayer"]) subprocess.Popen(["pkill", "-f", "omxplayer"])
return render_template("index.html", sounds=sounds) if request.path == "/edit":
edit = True
else:
edit = False
return render_template("index.html", sounds=sounds, tags=tags, edit=edit)
@app.route("/edit/<sound>", methods=["GET", "POST"])
def edit(sound):
tags = queryDB("""\
SELECT
tag.id,
tag.name,
checked.id IS NOT NULL AS checked
FROM
tag
LEFT OUTER JOIN (
SELECT
tag.id
FROM
tag
JOIN
button_tags
ON
fk_tag = tag.id
WHERE fk_button = (
SELECT
button.id
FROM
button
WHERE
button.file = ?
)
) AS checked
ON tag.id = checked.id""", (sound,))
if request.method == "POST":
if not request.form.get("cancel"):
buttonId = queryDB("SELECT id FROM button WHERE file = ?", (sound,), True)
if buttonId is None:
queryDB("INSERT INTO button (file) VALUES (?)", (sound,))
getDB().commit()
buttonId = queryDB("SELECT id FROM button WHERE file = ?", (sound,), True)
for tag in tags:
checkbox = 1 if request.form.get("cb-{}".format(tag["name"])) else 0
if tag["checked"] < checkbox:
queryDB("INSERT OR REPLACE INTO button_tags (fk_button, fk_tag) VALUES (?, ?)", (buttonId[0], tag["id"]))
getDB().commit()
elif tag["checked"] > checkbox:
queryDB("DELETE FROM button_tags WHERE fk_button = ? AND fk_tag = ?", (buttonId[0], tag["id"]))
getDB().commit()
return redirect("/edit")
return render_template("edit.html", sound=sound, tags=tags)
@app.route("/sounds/<path:name>") @app.route("/sounds/<path:name>")
def sounds(name): def sounds(name):

View File

@ -20,8 +20,7 @@ h1 {
} }
form { form {
text-align: center; padding: 5px;
display: inline;
} }
input, button { input, button {
@ -49,13 +48,70 @@ button:active {
color: #000; color: #000;
} }
.editform {
display: table;
margin-left: auto;
margin-right: auto;
}
.editform input {
margin: 5px;
}
.editform input[type="text"] {
display: block;
}
.editform input[type="checkbox"] {
display: block;
float: left;
height: 15px;
margin: 5px;
width: 15px;
}
.editform .tags li {
clear: left;
display: block;
line-height: 20px;
vertical-align: middle;
}
.editform .tags li span {
line-height: 25px;
}
.tags {
padding-left: 0;
}
.tags li {
display: inline-block;
list-style-type: none;
}
.tags li a {
background-color: #333;
border: 1px solid #999;
border-radius: 2px;
color: #999;
font-size: 0.9em;
margin: 5px;
padding: 2px 5px 2px 5px;
}
.tags li a:hover {
background-color: #eee;
color: #333;
}
label { label {
vertical-align: middle; vertical-align: middle;
} }
nav { nav {
text-align: center;
margin: 1em; margin: 1em;
text-align: center;
} }
nav a { nav a {
@ -91,8 +147,26 @@ section {
cursor: pointer; cursor: pointer;
} }
nav .extra {
position: absolute;
padding: 1em;
top: 1em;
right: 1em;
}
nav .extra > a {
background-color: #2ecc40;
border: 1px solid #000;
border-radius: 5px;
color: #333;
display: inline-block;
padding: 1em;
}
.sound { .sound {
display: inline-block; display: inline-block;
border: 1px solid transparent;
margin: 5px;
} }
.sound a { .sound a {
@ -113,6 +187,8 @@ section {
} }
.sound a:hover { .sound a:hover {
background-color: #ff4136;
box-shadow: inset 5px 5px 5px rgba(0, 0, 0, 0.6);
cursor: pointer; cursor: pointer;
} }
@ -127,6 +203,16 @@ section {
box-shadow: inset 5px 5px 5px rgba(0, 0, 0, 0.6) !important; box-shadow: inset 5px 5px 5px rgba(0, 0, 0, 0.6) !important;
} }
.edit {
border: 1px dashed #eee;
box-sizing: border-box;
margin: 5px;
}
.sound.edit a:hover {
box-shadow: inset -5px -5px 5px rgba(0, 0, 0, 0.6);
}
.unselectable { .unselectable {
-webkit-touch-callout: none; -webkit-touch-callout: none;
-webkit-user-select: none; -webkit-user-select: none;
@ -139,4 +225,3 @@ section {
#local-mode-button .local { #local-mode-button .local {
color: red; color: red;
} }

View File

@ -18,10 +18,13 @@ function sleep(ms) {
} }
ready(function() { ready(function() {
var sections = document.querySelectorAll("section"); hideSections();
var nav = document.querySelectorAll("nav a");
sections[0].style.display = "block"; var sections = document.querySelectorAll("section");
var nav = document.querySelectorAll("nav > a");
if (sections.length > 0)
sections[0].style.display = "block";
nav.forEach(function(item) { nav.forEach(function(item) {
item.onclick = function(e) { item.onclick = function(e) {
@ -39,21 +42,8 @@ ready(function() {
var searchfield = document.querySelector("#search"); var searchfield = document.querySelector("#search");
searchfield.focus(); searchfield.focus();
searchfield.addEventListener("keyup", function() { searchFilter(searchfield, ".sound", "inline-block");
var buttons = document.querySelectorAll(".sound"); searchFilter(searchfield, ".tag", "block");
buttons.forEach(function(item) {
item.style.display = "inline-block";
});
buttons.forEach(function(item) {
var name = item.firstChild.innerHTML;
if (name.toLowerCase().indexOf(searchfield.value.toLowerCase()) === -1) {
item.style.display = "none";
}
});
});
var reset = document.querySelector("#reset"); var reset = document.querySelector("#reset");
var sounds_nav = document.querySelector("#sounds-nav"); var sounds_nav = document.querySelector("#sounds-nav");
@ -64,6 +54,26 @@ ready(function() {
addKeyListeners(); addKeyListeners();
}); });
function searchFilter(searchfield, itemSelector, unHideStyle) {
if (searchfield !== null) {
searchfield.addEventListener("keyup", function() {
var items = document.querySelectorAll(itemSelector);
items.forEach(function(item) {
item.style.display = unHideStyle;
});
items.forEach(function(item) {
var name = item.firstChild.innerHTML;
if (name.toLowerCase().indexOf(searchfield.value.toLowerCase()) === -1) {
item.style.display = "none";
}
});
});
}
}
/* /*
* Adds all necessary KeyListeners to document * Adds all necessary KeyListeners to document
*/ */

56
templates/base.html Normal file
View File

@ -0,0 +1,56 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>WIAI Soundboard</title>
<link rel="stylesheet" href="/static/main.css" />
</head>
<body>
{% block navigation %}
<nav>
<a href="#sounds">Sounds</a>
<a href="#youtube">YouTube</a>
<a href="#voice">Voice</a>
{% if not edit %}
<div class="extra"><a href="/edit">Edit</a></div>
{% else %}
<div class="extra"><a href="/">Done</a></div>
{% endif %}
</nav>
{% endblock %}
<content>
{% block content %}
<section id="sounds">
<div><input type="text" id="search" /><span id="reset"></span></div>
{% for sound in sounds %}
<div class="sound{{ ' edit' if edit }}"><a href="{{ '/' + ('edit' if edit else 'play') + '/' + sound | urlencode }}">{{ sound.split('.', 1)[0] }}</a></div>
{% endfor %}
</section>
<section id="youtube">
<form action="/" method="GET">
<label for="video">YouTube URL</label>
<input type="text" name="video" />
<input type="submit" value="Play" />
</form>
<form action="/" method="GET">
<input type="hidden" name="killvideo" value="yes" />
<input type="submit" name="submit" value="Terminate videos" />
</form>
</section>
<section id="voice">
<form action="/say/" method="POST">
<input type="text" name="text" />
<input type="text" name="voice" placeholder="DE" />
<label for="speed">Speed</label>
<input type="range" name="speed" min="80" max="450" step="10" value="160" id="speed" />
<label for="pitch">Pitch</label>
<input type="range" name="pitch" min="0" max="99" step="1" value="50" id="pitch" />
<input type="submit" value="Voice" />
</form>
</section>
{% endblock %}
</content>
<script src="/static/main.js"></script>
</body>
</html>

18
templates/edit.html Normal file
View File

@ -0,0 +1,18 @@
{% extends "base.html" %}
{% block navigation %}
{% endblock %}
{% block content %}
<div class="editform">
<h1>Edit {{ sound }}</h1>
<form action="{{ request.path }}" method="POST">
<input type="text" id="search" placeholder="Search Tag" />
<ul class="tags">
{% for tag in tags %}
<li class="tag"><span>{{ tag.name }}</span><input type="checkbox" name="cb-{{ tag.name }}" {{ 'checked="true" '|safe if tag.checked }}/></li>
{% endfor %}
</ul>
<input type="submit" name="cancel" value="Cancel" />
<input type="submit" name="submit" value="Save" />
</form>
</div>
{% endblock %}

View File

@ -33,6 +33,11 @@
<a href="#sounds" id="sounds-nav">Sounds</a> <a href="#sounds" id="sounds-nav">Sounds</a>
<a href="#streams">Streams</a> <a href="#streams">Streams</a>
<a href="#voice">Voices</a> <a href="#voice">Voices</a>
{% if not edit %}
<div class="extra"><a href="/edit">Edit</a></div>
{% else %}
<div class="extra"><a href="/">Done</a></div>
{% endif %}
</nav> </nav>
<content> <content>
<section id="sounds" style="display:none;"> <section id="sounds" style="display:none;">
@ -40,10 +45,15 @@
<a onclick="toggleLocalMode();" id="local-mode-button"><button title="play sounds on server">[S]</button></a> <a onclick="toggleLocalMode();" id="local-mode-button"><button title="play sounds on server">[S]</button></a>
<input type="text" id="search" autofocus/> <input type="text" id="search" autofocus/>
<div id="reset"></div> <div id="reset"></div>
</div> <ul class="tags">
{% for tag in tags %}
<li><a href="#">{{ tag.name }}</a></li>
{% endfor %}
</ul>
</div><!--
{% for sound in sounds %} {% for sound in sounds %}
<div class="sound"><a title="{{ sound.split('.', 1)[0] }}" tabindex="0" class="unselectable" onclick="playSound('{{ sound | urlencode }}');">{{ sound.split('.', 1)[0] }}</a></div> --><div class="sound{{ ' edit' if edit }}"><a title="{{ sound.split('.', 1)[0] }}" tabindex="0" class="unselectable" {{ 'href=/edit/' + sound | urlencode if edit else 'onclick=playSound(\'' + sound | urlencode + '\');' }}>{{ sound.split('.', 1)[0] }}</a></div><!--
{% endfor %} {% endfor %}-->
</section> </section>
<section id="streams" style="display:none;"> <section id="streams" style="display:none;">
<div> <div>
@ -71,4 +81,3 @@
<script src="/static/main.js"></script> <script src="/static/main.js"></script>
</body> </body>
</html> </html>