diff --git a/.gitignore b/.gitignore index bea195b..7a3ba1a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ # Project specific files config.py +metadata.sqlite* diff --git a/config.py.example b/config.py.example index 841b7df..63b42bd 100644 --- a/config.py.example +++ b/config.py.example @@ -1 +1,2 @@ path = "/home/pi/sounds" +db = "metadata.sqlite" diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..92f2c6c --- /dev/null +++ b/schema.sql @@ -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) +); diff --git a/soundboard.py b/soundboard.py index 1935abf..8c1b380 100644 --- a/soundboard.py +++ b/soundboard.py @@ -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,50 +11,138 @@ 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/") @app.route("/say/", methods=["POST"]) @app.route("/say/") def index(sound=None, text=None, video=None): - sounds = [os.fsencode(file).decode() for file in os.listdir(config.path)] - sounds = sorted(sounds) + sounds = [os.fsencode(file).decode() for file in os.listdir(config.path)] + sounds = sorted(sounds) - 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) + tags = queryDB("SELECT name FROM tag ORDER BY name COLLATE NOCASE") - if text is None: - text = request.form.get("text") + 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) - if text is not None: - 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" + if text is None: + text = request.form.get("text") - subprocess.Popen(["espeak", "-v", voice, "-s", speed, "-p", pitch, text.encode("utf-8")], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - return redirect("/") + if text is not None: + 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": - subprocess.Popen(["omxplayer", video], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - else: - url = subprocess.check_output(["youtube-dl", "-g", "-f", "mp4", video]).decode() - subprocess.Popen(["omxplayer", url.split("\n")[1]]) - subprocess.Popen(["omxplayer", "-b", url.split("\n")[0]]) + if video is not None: + if video[-4:] == ".mp3": + subprocess.Popen(["omxplayer", video], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + else: + url = subprocess.check_output(["youtube-dl", "-g", "-f", "mp4", video]).decode() + 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"]: - subprocess.Popen(["pkill", "-f", "omxplayer"]) + 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/", 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/") def sounds(name): diff --git a/static/main.css b/static/main.css index f758fc3..c24adad 100644 --- a/static/main.css +++ b/static/main.css @@ -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; } - diff --git a/static/main.js b/static/main.js index 50d8fd0..def6d3c 100644 --- a/static/main.js +++ b/static/main.js @@ -18,10 +18,13 @@ function sleep(ms) { } ready(function() { - var sections = document.querySelectorAll("section"); - var nav = document.querySelectorAll("nav a"); + hideSections(); - 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) { item.onclick = function(e) { @@ -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 */ diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..b7914d8 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,56 @@ + + + + + + WIAI Soundboard + + + + {% block navigation %} + + {% endblock %} + + {% block content %} +
+
+ {% for sound in sounds %} + + {% endfor %} +
+
+
+ + + +
+
+ + +
+
+
+
+ + + + + + + +
+
+ {% endblock %} +
+ + + diff --git a/templates/edit.html b/templates/edit.html new file mode 100644 index 0000000..62f4a45 --- /dev/null +++ b/templates/edit.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% block navigation %} +{% endblock %} +{% block content %} +
+

Edit {{ sound }}

+
+ +
    + {% for tag in tags %} +
  • {{ tag.name }}
  • + {% endfor %} +
+ + +
+
+{% endblock %} diff --git a/templates/index.html b/templates/index.html index 2d381e3..f3647cd 100644 --- a/templates/index.html +++ b/templates/index.html @@ -33,6 +33,11 @@ Sounds Streams Voices + {% if not edit %} + + {% else %} + + {% endif %}