forked from server/soundboard
Merge branch 'metadata' of https://git.wiai.de/mmueller/soundboard
This commit is contained in:
commit
a18db979bb
1
.gitignore
vendored
1
.gitignore
vendored
@ -8,3 +8,4 @@
|
||||
|
||||
# Project specific files
|
||||
config.py
|
||||
metadata.sqlite*
|
||||
|
||||
@ -1 +1,2 @@
|
||||
path = "/home/pi/sounds"
|
||||
db = "metadata.sqlite"
|
||||
|
||||
18
schema.sql
Normal file
18
schema.sql
Normal 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)
|
||||
);
|
||||
@ -1,7 +1,9 @@
|
||||
import os
|
||||
import sys
|
||||
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
|
||||
|
||||
@ -9,9 +11,38 @@ app = Flask(__name__)
|
||||
app.jinja_env.trim_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("/edit")
|
||||
@app.route("/play/<sound>")
|
||||
@app.route("/say/", methods=["POST"])
|
||||
@app.route("/say/<text>")
|
||||
@ -19,6 +50,8 @@ def index(sound=None, text=None, video=None):
|
||||
sounds = [os.fsencode(file).decode() for file in os.listdir(config.path)]
|
||||
sounds = sorted(sounds)
|
||||
|
||||
tags = queryDB("SELECT name FROM tag ORDER BY name COLLATE NOCASE")
|
||||
|
||||
if sound is not None and sound in sounds:
|
||||
subprocess.Popen(["omxplayer", os.path.join(config.path, sound).encode("utf-8")], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
@ -39,7 +72,6 @@ def index(sound=None, text=None, video=None):
|
||||
video = request.args.get("video")
|
||||
|
||||
if video is not None:
|
||||
|
||||
if video[-4:] == ".mp3":
|
||||
subprocess.Popen(["omxplayer", video], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
else:
|
||||
@ -52,7 +84,65 @@ def index(sound=None, text=None, video=None):
|
||||
if killvideo is not None and killvideo in ["1", "true", "yes"]:
|
||||
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>")
|
||||
def sounds(name):
|
||||
|
||||
@ -20,8 +20,7 @@ h1 {
|
||||
}
|
||||
|
||||
form {
|
||||
text-align: center;
|
||||
display: inline;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
input, button {
|
||||
@ -49,13 +48,70 @@ button:active {
|
||||
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 {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
nav {
|
||||
text-align: center;
|
||||
margin: 1em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
nav a {
|
||||
@ -91,8 +147,26 @@ section {
|
||||
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 {
|
||||
display: inline-block;
|
||||
border: 1px solid transparent;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.sound a {
|
||||
@ -113,6 +187,8 @@ section {
|
||||
}
|
||||
|
||||
.sound a:hover {
|
||||
background-color: #ff4136;
|
||||
box-shadow: inset 5px 5px 5px rgba(0, 0, 0, 0.6);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@ -127,6 +203,16 @@ section {
|
||||
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 {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
@ -139,4 +225,3 @@ section {
|
||||
#local-mode-button .local {
|
||||
color: red;
|
||||
}
|
||||
|
||||
|
||||
@ -18,9 +18,12 @@ function sleep(ms) {
|
||||
}
|
||||
|
||||
ready(function() {
|
||||
var sections = document.querySelectorAll("section");
|
||||
var nav = document.querySelectorAll("nav a");
|
||||
hideSections();
|
||||
|
||||
var sections = document.querySelectorAll("section");
|
||||
var nav = document.querySelectorAll("nav > a");
|
||||
|
||||
if (sections.length > 0)
|
||||
sections[0].style.display = "block";
|
||||
|
||||
nav.forEach(function(item) {
|
||||
@ -39,21 +42,8 @@ ready(function() {
|
||||
var searchfield = document.querySelector("#search");
|
||||
searchfield.focus();
|
||||
|
||||
searchfield.addEventListener("keyup", function() {
|
||||
var buttons = document.querySelectorAll(".sound");
|
||||
|
||||
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";
|
||||
}
|
||||
});
|
||||
});
|
||||
searchFilter(searchfield, ".sound", "inline-block");
|
||||
searchFilter(searchfield, ".tag", "block");
|
||||
|
||||
var reset = document.querySelector("#reset");
|
||||
var sounds_nav = document.querySelector("#sounds-nav");
|
||||
@ -64,6 +54,26 @@ ready(function() {
|
||||
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
|
||||
*/
|
||||
|
||||
56
templates/base.html
Normal file
56
templates/base.html
Normal 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
18
templates/edit.html
Normal 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 %}
|
||||
@ -33,6 +33,11 @@
|
||||
<a href="#sounds" id="sounds-nav">Sounds</a>
|
||||
<a href="#streams">Streams</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>
|
||||
<content>
|
||||
<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>
|
||||
<input type="text" id="search" autofocus/>
|
||||
<div id="reset">✕</div>
|
||||
</div>
|
||||
{% 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>
|
||||
<ul class="tags">
|
||||
{% for tag in tags %}
|
||||
<li><a href="#">{{ tag.name }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div><!--
|
||||
{% for sound in sounds %}
|
||||
--><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 %}-->
|
||||
</section>
|
||||
<section id="streams" style="display:none;">
|
||||
<div>
|
||||
@ -71,4 +81,3 @@
|
||||
<script src="/static/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user