Merge branch 'master' into metadata

This commit is contained in:
Martin Müller 2017-11-06 16:19:47 +01:00
commit 1a4526809d
27 changed files with 3118 additions and 69 deletions

View File

@ -11,14 +11,14 @@ Requirements
- youtube-dl
- espeak
. code::
.. code::
apt install espeak omxplayer python3.4 python3-pip
pip3 install virtualenv youtube-dl
It is highly recommended to install the soundboard and its dependencies into a virtual environment to keep the system clean. To do so first create a new virtual environment in the desired folder and install the remaining Python dependencies via the `pip` of the virtualenv:
. code::
.. code::
virtualenv py
py/bin/pip install -r requirements.txt
@ -28,7 +28,7 @@ Apache
It is also recommended to use Apache instead of the Python Flask web server.
. code::
.. code::
apt install apache2 libapache2-mod-wsgi-py3
@ -49,7 +49,7 @@ Development/Debugging
For development and debugging the internal web server of Flask can be used e.g. by running following command inside the project folder:
. code::
.. code::
FLASK_DEBUG=1 FLASK_APP=soundboard.py py/bin/flask run --host=0.0.0.0
@ -59,3 +59,28 @@ Production
For production deployment Apache (or other webservers like nginx, lighttpd etc.) should be used.
For Apache a sample configuration is provided in the folder `apache` which can be adapted.
Please keep in mind that you may have to adjust the path to the sound files in your apache config in order to enable local playback on the clients.
Usage
=====
Users can browse the webinterface, search for sounds and play them on the remote server or their local browser.
Additionally to the normal point and click usage there are also several keyboard shortcuts available.
Keyboard shortcuts
------------------
- Navigation items, input fields and sound buttons can be selected by using tab and shift+tab
- esc clears the search field
- enter plays the selected sound
- ctrl+k and ctrl+c kill all sounds
- ctrl+x switches between local and remote playback
- ctrl+enter kills sounds before the selected gets played
Live version
============
A list of sound requests and already added sounds can be found here: https://pad.wiai.de/p/soundboardTodos

View File

@ -1,10 +1,10 @@
Listen 5000
<VirtualHost *:5000>
#ServerName example.com
WSGIDaemonProcess soundboard user=www-data group=www-data threads=5
WSGIScriptAlias / /var/www/soundboard/soundboard.wsgi
Alias /sounds /home/pi/sounds
<Directory /var/www/soundboard>
WSGIProcessGroup soundboard
WSGIApplicationGroup %{GLOBAL}
@ -12,4 +12,14 @@ Listen 5000
Order deny,allow
Allow from all
</Directory>
<Directory /home/pi/sounds>
Options FollowSymLinks
AllowOverride All
Order deny,allow
Allow from all
Require all granted
</Directory>
</VirtualHost>

12
devserver.sh Executable file
View File

@ -0,0 +1,12 @@
#!/bin/sh
basedir=$(dirname "$0")
if [ ! -d "$basedir/py" ]; then
echo "Directory \"py\" with a Python virtual environment"
echo "does not exist. See README.rst on how to set it up."
else
export FLASK_DEBUG=1
export FLASK_APP="$basedir/soundboard.py"
"$basedir"/py/bin/flask run
fi

View File

@ -3,7 +3,7 @@ import sys
import subprocess
import sqlite3
from flask import Flask, render_template, request, redirect, url_for, g
from flask import Flask, render_template, request, redirect, url_for, send_from_directory, g
import config
@ -66,7 +66,7 @@ def index(sound=None, text=None, video=None):
pitch = request.form.get("pitch", default="")
pitch = pitch if pitch.strip() != "" else "50"
subprocess.Popen(["espeak", "-v", voice, "-s", speed, "-p", pitch, text], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
subprocess.Popen(["espeak", "-v", voice, "-s", speed, "-p", pitch, text.encode("utf-8")], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
return redirect("/")
video = request.args.get("video")
@ -140,3 +140,7 @@ ON tag.id = checked.id""", (sound,))
return redirect("/edit")
return render_template("edit.html", sound=sound, tags=tags)
@app.route("/sounds/<path:name>")
def sounds(name):
return send_from_directory(config.path, name)

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
static/favicon/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

2801
static/howler.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
htl, body {
html, body {
margin: 0;
}
@ -23,7 +23,7 @@ form {
padding: 5px;
}
input {
input, button {
background-color: #666;
border: 1px solid #999;
border-radius: 5px;
@ -33,6 +33,21 @@ input {
vertical-align: middle;
}
button {
margin: 5px;
}
button:hover {
background-color: #999;
border: 1px solid #666;
}
button:active {
background-color: #999;
border: 1px solid #666;
color: #000;
}
.editform {
display: table;
margin-left: auto;
@ -90,7 +105,7 @@ input {
color: #333;
}
.youtube label {
label {
vertical-align: middle;
}
@ -99,7 +114,7 @@ nav {
text-align: center;
}
nav > a {
nav a {
background-color: #eee;
border: 1px solid #000;
border-radius: 5px;
@ -121,12 +136,14 @@ section {
width: 100%;
}
.reset {
#reset {
font-size: 2em;
padding-left: 5px;
display: inline;
vertical-align: middle;
}
.reset:hover {
#reset:hover {
cursor: pointer;
}
@ -172,6 +189,18 @@ nav .extra > a {
.sound a:hover {
background-color: #ff4136;
box-shadow: inset 5px 5px 5px rgba(0, 0, 0, 0.6);
cursor: pointer;
}
.sound a:active {
background-color: #ff4136;
box-shadow: inset 5px 5px 5px rgba(0, 0, 0, 0.6);
cursor: pointer;
}
.sound-pressed {
background-color: #ff4136 !important;
box-shadow: inset 5px 5px 5px rgba(0, 0, 0, 0.6) !important;
}
.edit {
@ -184,10 +213,15 @@ nav .extra > a {
box-shadow: inset -5px -5px 5px rgba(0, 0, 0, 0.6);
}
.table-row {
display: table-row;
.unselectable {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.table-cell {
display: table-cell;
#local-mode-button .local {
color: red;
}

View File

@ -1,3 +1,10 @@
/* jshint esversion: 6 */
var localModeEnabled = false;
// References to howler objects
var howlerSounds = [];
function ready(fn) {
if (document.attachEvent ? document.readyState === "complete" : document.readyState !== "loading") {
fn();
@ -6,6 +13,10 @@ function ready(fn) {
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
ready(function() {
hideSections();
@ -29,38 +40,18 @@ ready(function() {
});
var searchfield = document.querySelector("#search");
searchfield.focus();
searchFilter(searchfield, ".sound", "inline-block");
searchFilter(searchfield, ".tag", "block");
var reset = document.querySelector("#sounds .reset");
var reset = document.querySelector("#reset");
var sounds_nav = document.querySelector("#sounds-nav");
if (reset !== null) {
reset.addEventListener("click", function() {
var buttons = document.querySelectorAll(".sound");
reset.addEventListener("click", resetSearch, false);
sounds_nav.addEventListener("click", resetSearch, false);
buttons.forEach(function(item) {
item.style.display = "inline-block";
});
searchfield.value = "";
});
}
var taginputs = document.querySelectorAll(".taginput");
if (taginputs.length > 0) {
taginputs.forEach(function(taginput) {
taginput.addEventListener("keydown", function(e) {
if (e.target == taginput && (e.which == 13 || e.keyCode == 13)) {
e.preventDefault();
} else if (e.which == 32 || e.keyCode == 13) {
console.log("Space");
addTag(e.target);
}
});
});
}
addKeyListeners();
});
function searchFilter(searchfield, itemSelector, unHideStyle) {
@ -74,7 +65,6 @@ function searchFilter(searchfield, itemSelector, unHideStyle) {
items.forEach(function(item) {
var name = item.firstChild.innerHTML;
console.log(name);
if (name.toLowerCase().indexOf(searchfield.value.toLowerCase()) === -1) {
item.style.display = "none";
@ -84,6 +74,68 @@ function searchFilter(searchfield, itemSelector, unHideStyle) {
}
}
/*
* Adds all necessary KeyListeners to document
*/
function addKeyListeners() {
// keyboard listener for global key strokes/inputs
document.onkeydown = function(evt) {
evt = evt || window.event;
if (evt.keyCode == 27) {
// esc key
resetSearch();
} else if ((evt.keyCode == 75 || evt.keyCode == 67) && evt.ctrlKey) {
// ctrl+k and ctrl+c key binding
killAllAudio();
} else if (evt.keyCode == 88 && evt.ctrlKey){
// ctrl+x key binding
toggleLocalMode();
}
};
// keyboard listener for playing sounds using enter key
var buttons = document.querySelectorAll(".sound");
buttons.forEach(function(item) {
item.firstChild.addEventListener('keypress', async function (e) {
var key = e.keyCode;
var source = e.target;
// keylistener for enter or ctrl+enter (which is k10 on chrome)
if (key === 13 || (key === 10 && e.ctrlKey)) {
if (e.ctrlKey){
killAllAudio();
await sleep(100);
}
source.classList.add("sound-pressed");
source.onclick();
await sleep(300);
source.classList.remove("sound-pressed");
}
});
});
}
function resetSearch() {
var searchfield = document.querySelector("#search");
var buttons = document.querySelectorAll(".sound");
buttons.forEach(function(item) {
item.style.display = "inline-block";
});
searchfield.value = "";
searchfield.focus();
}
/*
* Reads the stream url from input with id #streaming-url and forwards it to the server using AJAX.
*/
function playStream() {
var streamUrl = document.querySelector("#streaming-url").value;
ajaxRequest("/?video="+encodeURI(streamUrl));
}
function hideSections() {
var sections = document.querySelectorAll("section");
@ -91,3 +143,67 @@ function hideSections() {
item.style.display = "none";
});
}
function ajaxRequest(url) {
var ajaxRequest;
try {
ajaxRequest = new XMLHttpRequest();
ajaxRequest.open("GET", url, true);
ajaxRequest.send(null);
} catch (e) {
alert("Unfortunately we were unable to handle your request. Please try again later and contact the server administrator if the problem persists.");
return false;
}
}
/*
* Switches between local and remote playback mode.
* Additionally changes the button text and its color so that the user has a visible feedback.
*/
function toggleLocalMode() {
toggleButton = document.getElementById("local-mode-button").children[0];
if (localModeEnabled) {
localModeEnabled = false;
toggleButton.classList.remove("local");
toggleButton.innerHTML = "[S]";
toggleButton.title = "play sounds on server";
} else {
localModeEnabled = true;
toggleButton.classList.add("local");
toggleButton.innerHTML = "[L]";
toggleButton.title = "play sounds locally";
}
}
/*
* Either plays the given sound file locally by using howler.js or forwards the request to the remote server using AJAX.
*/
function playSound(filename) {
if (localModeEnabled){
// play local audio using howler.js
var sound = new Howl({
src: ['/sounds/' + filename]
});
sound.play();
howlerSounds.push(sound);
} else {
ajaxRequest("/play/" + filename);
}
}
function killAllHowlerAudio() {
for (var i = 0; i < howlerSounds.length; i++) {
howlerSounds[i].stop();
}
}
/*
* Kills all local or remote audio, depending on localModeEnabled
*/
function killAllAudio() {
if (localModeEnabled) {
killAllHowlerAudio();
} else {
ajaxRequest("/?killvideo=yes");
}
}

View File

@ -1,28 +1,71 @@
{% extends "base.html" %}
{% block content %}
<section id="sounds">
<div><input type="text" id="search" /><span class="reset"></span></div>
<ul class="tags">
{% for tag in tags %}
<li><a href="#">{{ tag.name }}</a></li>
{% endfor %}
</ul>
{% 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 %}
<!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" />
<!-- favicon clipart source: https://openclipart.org/detail/190592/button -->
<link rel="apple-touch-icon-precomposed" sizes="57x57" href="/static/favicon/apple-touch-icon-57x57.png" />
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="/static/favicon/apple-touch-icon-114x114.png" />
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="/static/favicon/apple-touch-icon-72x72.png" />
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="/static/favicon/apple-touch-icon-144x144.png" />
<link rel="apple-touch-icon-precomposed" sizes="60x60" href="/static/favicon/apple-touch-icon-60x60.png" />
<link rel="apple-touch-icon-precomposed" sizes="120x120" href="/static/favicon/apple-touch-icon-120x120.png" />
<link rel="apple-touch-icon-precomposed" sizes="76x76" href="/static/favicon/apple-touch-icon-76x76.png" />
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="/static/favicon/apple-touch-icon-152x152.png" />
<link rel="icon" type="image/png" href="/static/favicon/favicon-196x196.png" sizes="196x196" />
<link rel="icon" type="image/png" href="/static/favicon/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/png" href="/static/favicon/favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="/static/favicon/favicon-16x16.png" sizes="16x16" />
<link rel="icon" type="image/png" href="/static/favicon/favicon-128.png" sizes="128x128" />
<meta name="application-name" content="WIAI Soundboard"/>
<meta name="msapplication-TileColor" content="#FFFFFF" />
<meta name="msapplication-TileImage" content="/static/favicon/mstile-144x144.png" />
<meta name="msapplication-square70x70logo" content="/static/favicon/mstile-70x70.png" />
<meta name="msapplication-square150x150logo" content="/static/favicon/mstile-150x150.png" />
<meta name="msapplication-wide310x150logo" content="/static/favicon/mstile-310x150.png" />
<meta name="msapplication-square310x310logo" content="/static/favicon/mstile-310x310.png" />
</head>
<body>
<nav>
<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;">
<div>
<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>
<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 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="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 id="streams" style="display:none;">
<div>
<form>
<label for="video">Stream URL</label>
<input id="streaming-url" type="text" name="video" placeholder="paste stream url here..." />
</form>
<button onclick="playStream();">Play!</button>
</div>
<button name="submit" onclick="killAllAudio();">Terminate videos</button>
</section>
<section id="voice">
<section id="voice" style="display:none;">
<form action="/say/" method="POST">
<input type="text" name="text" />
<input type="text" name="voice" placeholder="DE" />
@ -33,4 +76,8 @@
<input type="submit" value="Voice" />
</form>
</section>
{% endblock %}
</content>
<script src="/static/howler.js"></script>
<script src="/static/main.js"></script>
</body>
</html>