Compare commits

..

61 Commits

Author SHA1 Message Date
2a8040642a Merge branch 'legacy' into feature/legacy/mariadb_database 2023-03-28 01:26:24 +02:00
cbf7aad8ac Fix Type errors 2023-03-28 01:25:33 +02:00
d5a21b82de Add GitHub Link to footer 2023-03-28 01:00:41 +02:00
551536bcb4 Update bootstrap-tables 2023-03-28 01:00:21 +02:00
b8220732ee Introduce "dev environment" and dotenv
There are now two dockerfiles. One for production, one for development.
All configuration is now also handled through dotenv files,
which these dotenv files, as well as the included VSCode launch tasks
use.
2023-03-28 00:54:04 +02:00
831166f38b Intall dependencies using requirements.txt 2023-03-27 23:38:22 +02:00
41a24ad9ce readd main.py 2023-03-27 01:02:26 +02:00
6efb234a1c Merge pull request #39 from PhoenixTwoFive/feature/legacy/mariadb_database
Feature/legacy/mariadb database
2022-10-08 19:51:56 +02:00
698c3717fd Remove outdated workflows 2022-10-08 19:50:35 +02:00
8dd90b728c MySQL Working, bug fixes. 2022-10-08 19:47:29 +02:00
a22960eae9 Fix DB Connection 2022-07-15 15:54:15 +02:00
bc44985fed Fix DBCONNSTRING 2022-07-15 15:35:58 +02:00
72914109c0 prune requirements 2022-07-15 15:33:42 +02:00
57ced69ddd Load DB Connection from ENV 2022-07-15 15:27:36 +02:00
fc78bdc4fe Fix types 2022-07-15 15:09:48 +02:00
adfd120636 Update package names for 22.04 2022-07-15 15:01:07 +02:00
67a9552fee Update ubuntu to get newer mariadb connector 2022-07-15 14:55:14 +02:00
3ae2803fcc Remove mysql and install mariadb 2022-07-15 14:51:05 +02:00
2138189dff python3 is still not default on Ubuntu? 2022-07-15 14:44:26 +02:00
9907a19066 Test 2022-07-15 14:43:16 +02:00
2a18f3dc2c Completely remove mariadb libs to hopefully fix conflicts 2022-07-15 14:36:47 +02:00
69e252e28a Add missing libraries to install 2022-07-15 14:35:52 +02:00
8151e615c0 Test 2022-07-15 14:34:07 +02:00
2a92c39450 Test 2022-07-15 14:32:51 +02:00
b88aac69e6 Fix.. 2022-07-15 14:32:06 +02:00
c9ad755a04 Fix? 2022-07-15 14:29:18 +02:00
3527ba4fd9 Fix dependencies 2022-07-15 14:27:39 +02:00
fb9677dc88 Fix github workflow 2022-07-15 14:23:17 +02:00
97dd80b03a Add or update the Azure App Service build and deployment workflow config 2022-07-15 14:16:06 +02:00
faad60346f Intial changes for MariaDB 2022-07-15 14:01:02 +02:00
3be307c0f9 Short commit hash on heroku 2022-07-15 02:59:33 +02:00
8cc9ed3645 Add version loading 2022-07-15 02:55:18 +02:00
108d70253c TEst 2022-07-15 02:34:41 +02:00
527c9d451d Refactor for heroku? 2022-07-15 02:31:58 +02:00
5beb752edf forgot gunicorn 2022-07-15 02:24:35 +02:00
589e50120b Refactor for heroku... 2022-07-15 02:22:36 +02:00
f421423636 Refactor for heroku 2022-07-15 02:13:41 +02:00
f6901a6175 Change 2022-07-15 02:11:12 +02:00
895dd14073 Refactor for heroku 2022-07-15 02:07:40 +02:00
95838885e7 Fix parsing of songlist download page for CSV file link 2022-04-07 21:32:11 +02:00
99c7917573 Merge branch 'legacy_feature_transferred_tagging' into legacy 2021-10-09 19:37:53 +02:00
23e7bf71bc Add transfer status toggle button 2021-10-09 19:36:28 +02:00
360b61fe21 Better version identification 2021-10-09 18:16:55 +02:00
bf1b711abe Fix webmanifest 2021-10-09 01:04:23 +02:00
65635f57a8 Fix overly agressive caching 2021-10-09 01:04:15 +02:00
013dcecba9 fix launch configs 2021-10-09 00:03:56 +02:00
d79eebc949 Add favicon, fix caching 2021-10-09 00:03:46 +02:00
c203317320 Add Version identification to footer 2021-10-08 23:21:41 +02:00
4b70b70a1d Merge branch 'legacy_feature_client_identification_queue_limit' into stable 2021-10-08 22:44:32 +02:00
69f84332e6 style fixes 2021-10-06 02:16:24 +02:00
6a7b07ac9f data type error in config variables 2021-10-06 02:02:23 +02:00
ba4b5fb57c Bessere Formulierung von Fehlern 2021-10-06 02:00:09 +02:00
e6bc974a6f Eintragungstoggle fix 2021-10-06 01:59:51 +02:00
569daf0a04 Einstellungsseite hinzugefügt 2021-10-06 01:59:30 +02:00
78a39b8d17 Add client ids and quota as well as queue limit 2021-10-06 00:45:57 +02:00
14ce3e07f5 Force no scaling 2021-10-05 22:49:00 +02:00
8f769ac7e3 Configure HTTP->HTTPS redirect 2021-08-20 16:58:35 +02:00
4b2be56b3f Cosmetics 2021-08-20 15:58:45 +02:00
87a632a661 Fix breakage by not pinning versions 2021-08-20 15:47:30 +02:00
9ab1e9b841 Adapt for Google Cloud Deployment 2021-08-20 15:47:03 +02:00
080648d587 Add node_modules to .gitignore 2020-08-14 12:46:14 +02:00
30 changed files with 864 additions and 435 deletions

14
.env.dev Normal file
View File

@ -0,0 +1,14 @@
# MariaDB
MARIADB_ROOT_PASSWORD=mariadb_root_password
MARIADB_ROOT_HOST=localhost
MARIADB_DATABASE=karaoqueue
MARIADB_USER=karaoqueue
MARIADB_PASSWORD=mariadb_karaoqueue_password
# Karaoqueue
DEPLOYMENT_PLATFORM=Docker
DBSTRING=mysql://karaoqueue:mariadb_karaoqueue_password@127.0.0.1:3306/karaoqueue
BASIC_AUTH_USERNAME=admin
BASIC_AUTH_PASSWORD=change_me
ENTRY_QUOTA=3
MAX_QUEUE=20

10
.gitignore vendored
View File

@ -131,3 +131,13 @@ dmypy.json
# Test data
data/
# Node Modules
node_modules/
# Version identification file
.version
# Docker secrets
secrets.yml

64
.vscode/launch.json vendored
View File

@ -4,43 +4,83 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{"name":"Python: Flask","type":"python","request":"launch","module":"flask","env":{"FLASK_APP":"backend/app/main.py","FLASK_ENV":"development","FLASK_DEBUG":"1"},"args":["run","--no-debugger","--no-reload"],"jinja":true},
{"name":"Python: Flask (with reload)","type":"python","request":"launch","module":"flask","env":{"FLASK_APP":"backend/app/main.py","FLASK_ENV":"development","FLASK_DEBUG":"1"},"args":["run","--no-debugger"],"jinja":true},
{
"name": "Python: Flask (with reload, externally reachable)",
"preLaunchTask": "mariadb",
"name": "Python: Flask",
"type": "python",
"cwd": "${workspaceFolder}/backend",
"request": "launch",
"module": "flask",
"env": {
"FLASK_APP": "backend/app/main.py",
"FLASK_APP": "backend/app.py",
"FLASK_ENV": "development",
"FLASK_DEBUG": "1"
"FLASK_DEBUG": "1",
},
"envFile": "${workspaceFolder}/.env.dev",
"args": [
"run",
"--no-debugger",
"--host='0.0.0.0'"
"--no-reload"
],
"jinja": true
},
{
"name": "Python: Flask (externally reachable)",
"preLaunchTask": "mariadb",
"name": "Python: Flask (with reload)",
"type": "python",
"cwd": "${workspaceFolder}/backend",
"request": "launch",
"module": "flask",
"env": {
"FLASK_APP": "backend/app/main.py",
"FLASK_APP": "backend/app.py",
"FLASK_ENV": "development",
"FLASK_DEBUG": "1"
},
"envFile": "${workspaceFolder}/.env.dev",
"args": [
"run",
"--no-debugger"
],
"jinja": true
},
{
"preLaunchTask": "mariadb",
"name": "Python: Flask (with reload, externally reachable)",
"type": "python",
"cwd": "${workspaceFolder}/backend",
"request": "launch",
"module": "flask",
"env": {
"FLASK_APP": "backend/app.py",
"FLASK_ENV": "development",
"FLASK_DEBUG": "1"
},
"envFile": "${workspaceFolder}/.env.dev",
"args": [
"run",
"--no-debugger",
"--host=0.0.0.0"
],
"jinja": true
},
{
"preLaunchTask": "mariadb",
"name": "Python: Flask (externally reachable)",
"type": "python",
"cwd": "${workspaceFolder}/backend",
"request": "launch",
"module": "flask",
"env": {
"FLASK_APP": "backend/app.py",
"FLASK_ENV": "development",
"FLASK_DEBUG": "1"
},
"envFile": "${workspaceFolder}/.env.dev",
"args": [
"run",
"--no-debugger",
"--no-reload",
"--host='0.0.0.0'"
"--host=0.0.0.0"
],
"jinja": true
},

20
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,20 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "versiondump",
"type": "shell",
"command": "echo \"$(git rev-parse --abbrev-ref HEAD)-$(git describe)\"> ${workspaceFolder}/backend/.version",
"problemMatcher": []
},
{
"label": "mariadb",
"type": "shell",
"command": "docker-compose -f docker-compose.yml up --remove-orphans",
"isBackground": true,
"activeOnStart": false
}
]
}

5
Dockerfile Normal file
View File

@ -0,0 +1,5 @@
FROM tiangolo/uwsgi-nginx-flask:python3.7
COPY ./backend /app
RUN pip install -r /app/requirements.txt

19
backend/.gcloudignore Normal file
View File

@ -0,0 +1,19 @@
# This file specifies files that are *not* uploaded to Google Cloud Platform
# using gcloud. It follows the same syntax as .gitignore, with the addition of
# "#!include" directives (which insert the entries of the given .gitignore-style
# file at that point).
#
# For more information, run:
# $ gcloud topic gcloudignore
#
.gcloudignore
# If you would like to upload your .git directory, .gitignore file or files
# from your .gitignore file, remove the corresponding line
# below:
.git
.gitignore
# Python pycache:
__pycache__/
# Ignored by the build system
/setup.cfg

View File

@ -1,11 +0,0 @@
FROM tiangolo/uwsgi-nginx-flask:python3.7
RUN pip install requests
RUN pip install pandas
RUN pip install Flask-BasicAuth
RUN pip install bs4
COPY ./app /app

1
backend/Procfile Normal file
View File

@ -0,0 +1 @@
web: gunicorn wsgi:app

261
backend/app.py Normal file
View File

@ -0,0 +1,261 @@
from flask import Flask, render_template, abort, request, redirect, send_from_directory, jsonify
from flask.wrappers import Request, Response
import helpers
import database
import data_adapters
import os
import json
from flask_basicauth import BasicAuth
from helpers import nocache
app = Flask(__name__, static_url_path='/static')
basic_auth = BasicAuth(app)
accept_entries = True
@app.route("/")
def home():
if basic_auth.authenticate():
return render_template('main_admin.html', list=database.get_list(), auth=basic_auth.authenticate())
else:
return render_template('main.html', list=database.get_list(), auth=basic_auth.authenticate())
@app.route("/favicon.ico")
def favicon():
return send_from_directory(os.path.join(app.root_path, 'static'),
'favicon.ico', mimetype='image/vnd.microsoft.icon')
@app.route('/api/enqueue', methods=['POST'])
@nocache
def enqueue():
if not request.json:
print(request.data)
abort(400)
client_id = request.json['client_id']
if not helpers.is_valid_uuid(client_id):
print(request.data)
abort(400)
name = request.json['name']
song_id = request.json['id']
if request.authorization:
database.add_entry(name, song_id, client_id)
return Response('{"status":"OK"}', mimetype='text/json')
else:
if accept_entries:
if not request.json:
print(request.data)
abort(400)
client_id = request.json['client_id']
if not helpers.is_valid_uuid(client_id):
print(request.data)
abort(400)
name = request.json['name']
song_id = request.json['id']
if database.check_queue_length() < int(app.config['MAX_QUEUE']):
if database.check_entry_quota(client_id) < int(app.config['ENTRY_QUOTA']):
database.add_entry(name, song_id, client_id)
return Response('{"status":"OK"}', mimetype='text/json')
else:
return Response('{"status":"Du hast bereits ' + str(database.check_entry_quota(client_id)) + ' Songs eingetragen, dies ist das Maximum an Einträgen die du in der Warteliste haben kannst."}', mimetype='text/json', status=423)
else:
return Response('{"status":"Die Warteschlange enthält momentan ' + str(database.check_queue_length()) + ' Einträge und ist lang genug, bitte versuche es noch einmal wenn ein paar Songs gesungen wurden."}', mimetype='text/json', status=423)
else:
return Response('{"status":"Currently not accepting entries"}', mimetype='text/json', status=423)
@app.route("/list")
def songlist():
return render_template('songlist.html', list=database.get_song_list(), auth=basic_auth.authenticate())
@app.route("/settings")
@nocache
@basic_auth.required
def settings():
return render_template('settings.html', app=app, auth=basic_auth.authenticate())
@app.route("/settings", methods=['POST'])
@nocache
@basic_auth.required
def settings_post():
entryquota = request.form.get("entryquota")
maxqueue = request.form.get("maxqueue")
if entryquota.isnumeric() and int(entryquota) > 0: # type: ignore
app.config['ENTRY_QUOTA'] = int(entryquota) # type: ignore
else:
abort(400)
if maxqueue.isnumeric and int(maxqueue) > 0: # type: ignore
app.config['MAX_QUEUE'] = int(maxqueue) # type: ignore
else:
abort(400)
return render_template('settings.html', app=app, auth=basic_auth.authenticate())
@app.route("/api/queue")
@nocache
def queue_json():
list = data_adapters.dict_from_rows(database.get_list())
return Response(json.dumps(list, ensure_ascii=False).encode('utf-8'), mimetype='text/json')
@app.route("/plays")
@nocache
@basic_auth.required
def played_list():
return render_template('played_list.html', list=database.get_played_list(), auth=basic_auth.authenticate())
@app.route("/api/songs")
@nocache
def songs():
list = database.get_song_list()
return Response(json.dumps(list, ensure_ascii=False).encode('utf-8'), mimetype='text/json')
@app.route("/api/songs/update")
@nocache
@basic_auth.required
def update_songs():
database.delete_all_entries()
status = database.import_songs(
helpers.get_songs(helpers.get_catalog_url()))
print(status)
return Response('{"status": "%s" }' % status, mimetype='text/json')
@app.route("/api/songs/compl")
@nocache
def get_song_completions(input_string=""):
input_string = request.args.get('search', input_string)
if input_string != "":
print(input_string)
result = [list(x) for x in database.get_song_completions(input_string=input_string)]
return jsonify(result)
else:
return 400
@app.route("/api/entries/delete/<entry_id>")
@nocache
@basic_auth.required
def delete_entry(entry_id):
if database.delete_entry(entry_id):
return Response('{"status": "OK"}', mimetype='text/json')
else:
return Response('{"status": "FAIL"}', mimetype='text/json')
@app.route("/api/entries/delete", methods=['POST'])
@nocache
@basic_auth.required
def delete_entries():
if not request.json:
print(request.data)
abort(400)
return
updates = database.delete_entries(request.json)
if updates >= 0:
return Response('{"status": "OK", "updates": '+str(updates)+'}', mimetype='text/json')
else:
return Response('{"status": "FAIL"}', mimetype='text/json', status=400)
@app.route("/api/entries/mark_sung/<entry_id>")
@nocache
@basic_auth.required
def mark_sung(entry_id):
if database.add_sung_song(entry_id):
return Response('{"status": "OK"}', mimetype='text/json')
else:
return Response('{"status": "FAIL"}', mimetype='text/json')
@app.route("/api/entries/mark_transferred/<entry_id>")
@nocache
@basic_auth.required
def mark_transferred(entry_id):
if database.toggle_transferred(entry_id):
return Response('{"status": "OK"}', mimetype='text/json')
else:
return Response('{"status": "FAIL"}', mimetype='text/json')
@app.route("/api/entries/accept/<value>")
@nocache
@basic_auth.required
def set_accept_entries(value):
global accept_entries
if (value == '0' or value == '1'):
accept_entries = bool(int(value))
return Response('{"status": "OK"}', mimetype='text/json')
else:
return Response('{"status": "FAIL"}', mimetype='text/json', status=400)
@app.route("/api/entries/accept")
@nocache
def get_accept_entries():
global accept_entries
return Response('{"status": "OK", "value": '+str(int(accept_entries))+'}', mimetype='text/json')
@app.route("/api/played/clear")
@nocache
@basic_auth.required
def clear_played_songs():
if database.clear_played_songs():
return Response('{"status": "OK"}', mimetype='text/json')
else:
return Response('{"status": "FAIL"}', mimetype='text/json')
@app.route("/api/entries/delete_all")
@nocache
@basic_auth.required
def delete_all_entries():
if database.delete_all_entries():
return Response('{"status": "OK"}', mimetype='text/json')
else:
return Response('{"status": "FAIL"}', mimetype='text/json')
@app.route("/login")
@basic_auth.required
def admin():
return redirect("/", code=303)
@app.before_first_request
def activate_job():
helpers.load_dbconfig(app)
helpers.load_version(app)
helpers.create_data_directory()
database.create_entry_table()
database.create_song_table()
database.create_done_song_table()
database.create_list_view()
database.create_done_song_view()
helpers.setup_config(app)
@app.after_request
def add_header(response):
"""
Add headers to both force latest IE rendering engine or Chrome Frame,
and also to cache the rendered page for 10 minutes.
"""
if not 'Cache-Control' in response.headers:
response.headers['Cache-Control'] = 'private, max-age=600'
return response
@app.context_processor
def inject_version():
return dict(karaoqueue_version=app.config['VERSION'])
if __name__ == "__main__":
app.run(host='127.0.0.1', port=8080, debug=True)

19
backend/app.yaml Normal file
View File

@ -0,0 +1,19 @@
runtime: python39
manual_scaling:
# Das ist alles bloß dumm schnell zusammengehackt...
instances: 1
handlers:
# This configures Google App Engine to serve the files in the app's static
# directory.
- url: /static
static_dir: static
# This handler routes all requests not caught above to your main app. It is
# required when static routes are defined, but can be omitted (along with
# the entire handlers section) when there are no static files defined.
- url: /.*
script: auto
secure: always
redirect_http_response_code: 301

View File

@ -1,167 +0,0 @@
# -*- coding: utf_8 -*-
import sqlite3
import pandas
from io import StringIO
song_table = "songs"
entry_table = "entries"
index_label = "Id"
done_table = "done_songs"
def open_db():
conn = sqlite3.connect("data/test.db")
conn.execute('PRAGMA encoding = "UTF-8";')
return conn
def import_songs(song_csv):
print("Start importing Songs...")
df = pandas.read_csv(StringIO(song_csv), sep=';')
conn = open_db()
cur = conn.cursor()
df.to_sql(song_table, conn, if_exists='replace',
index=False)
cur.execute("SELECT Count(Id) FROM songs")
num_songs = cur.fetchone()[0]
conn.close()
print("Imported songs ({} in Database)".format(num_songs))
return("Imported songs ({} in Database)".format(num_songs))
def create_entry_table():
conn = open_db()
conn.execute('CREATE TABLE IF NOT EXISTS '+entry_table +
' (ID INTEGER PRIMARY KEY NOT NULL, Song_Id INTEGER NOT NULL, Name VARCHAR(255))')
conn.close()
def create_done_song_table():
conn = open_db()
conn.execute('CREATE TABLE IF NOT EXISTS '+done_table +
' (Song_Id INTEGER PRIMARY KEY NOT NULL, Plays INTEGER)')
conn.close()
def create_song_table():
conn = open_db()
conn.execute("CREATE TABLE IF NOT EXISTS \""+song_table+"""\" (
"Id" INTEGER,
"Title" TEXT,
"Artist" TEXT,
"Year" INTEGER,
"Duo" INTEGER,
"Explicit" INTEGER,
"Date Added" TEXT,
"Styles" TEXT,
"Languages" TEXT
)""")
conn.close()
def create_list_view():
conn = open_db()
conn.execute("""CREATE VIEW IF NOT EXISTS [Liste] AS
SELECT Name, Title, Artist, entries.Id, songs.Id
FROM entries, songs
WHERE entries.Song_Id=songs.Id""")
conn.close()
def create_done_song_view():
conn = open_db()
conn.execute("""CREATE VIEW IF NOT EXISTS [Abspielliste] AS
SELECT Artist || \" - \" || Title AS Song, Plays AS Wiedergaben
FROM songs, done_songs
WHERE done_songs.Song_Id=songs.Id""")
conn.close()
def get_list():
conn = open_db()
conn.row_factory = sqlite3.Row
cur = conn.cursor()
cur.execute("SELECT * FROM Liste")
return cur.fetchall()
def get_played_list():
conn = open_db()
cur = conn.cursor()
cur.execute("SELECT * FROM Abspielliste")
return cur.fetchall()
def get_song_list():
conn =open_db()
cur = conn.cursor()
cur.execute("SELECT Artist || \" - \" || Title AS Song, Id FROM songs;")
return cur.fetchall()
def get_song_completions(input_string):
conn = open_db()
cur = conn.cursor()
# Don't look, it burns...
prepared_string = "%{0}%".format(input_string).upper() # "Test" -> "%TEST%"
print(prepared_string)
cur.execute(
"SELECT Title || \" - \" || Artist AS Song, Id FROM songs WHERE REPLACE(REPLACE(REPLACE(REPLACE(UPPER( SONG ),'ö','Ö'),'ü','Ü'),'ä','Ä'),'ß','') LIKE (?) LIMIT 20;", (prepared_string,))
return cur.fetchall()
def add_entry(name,song_id):
conn = open_db()
cur = conn.cursor()
cur.execute(
"INSERT INTO entries (Song_Id,Name) VALUES(?,?);", (song_id,name))
conn.commit()
conn.close()
return
def add_sung_song(entry_id):
conn = open_db()
cur = conn.cursor()
cur.execute("""SELECT Song_Id FROM entries WHERE Id=?""",(entry_id,))
song_id = cur.fetchone()[0]
cur.execute("""INSERT OR REPLACE INTO done_songs (Song_Id, Plays)
VALUES("""+str(song_id)+""",
COALESCE(
(SELECT Plays FROM done_songs
WHERE Song_Id="""+str(song_id)+"), 0) + 1)"
)
conn.commit()
delete_entry(entry_id)
conn.close()
return True
def clear_played_songs():
conn = open_db()
cur = conn.cursor()
cur.execute("DELETE FROM done_songs")
conn.commit()
conn.close()
return True
def delete_entry(id):
conn = open_db()
cur = conn.cursor()
cur.execute("DELETE FROM entries WHERE id=?",(id,))
conn.commit()
conn.close()
return True
def delete_entries(ids):
idlist = []
for x in ids:
idlist.append( (x,) )
try:
conn = open_db()
cur = conn.cursor()
cur.executemany("DELETE FROM entries WHERE id=?", idlist)
conn.commit()
conn.close()
return cur.rowcount
except sqlite3.Error as error:
return -1
def delete_all_entries():
conn = open_db()
cur = conn.cursor()
cur.execute("DELETE FROM entries")
conn.commit()
conn.close()
return True

View File

@ -1,39 +0,0 @@
import requests
from bs4 import BeautifulSoup
import json
import os
data_directory = "data"
config_file = data_directory+"/config.json"
def create_data_directory():
if not os.path.exists(data_directory):
os.makedirs(data_directory)
def get_catalog_url():
r = requests.get('https://www.karafun.de/karaoke-song-list.html')
soup = BeautifulSoup(r.content, 'html.parser')
url = soup.findAll('a', href=True, text='Available in CSV format')[0]['href']
return url
def get_songs(url):
r = requests.get(url)
return r.text
def check_config_exists():
return os.path.isfile(config_file)
def setup_config(app):
if check_config_exists():
config = json.load(open(config_file))
with open(config_file, 'r') as handle:
config = json.load(handle)
print("Loaded existing config")
else:
config = {'username': 'admin', 'password': 'changeme'}
with open(config_file, 'w') as handle:
json.dump(config, handle, indent=4, sort_keys=True)
print("Wrote new config")
app.config['BASIC_AUTH_USERNAME'] = config['username']
app.config['BASIC_AUTH_PASSWORD'] = config['password']

View File

@ -1,155 +0,0 @@
from flask import Flask, render_template, Response, abort, request, redirect
import helpers
import database
import data_adapters
import os, errno
import json
from flask_basicauth import BasicAuth
app = Flask(__name__, static_url_path='/static')
basic_auth = BasicAuth(app)
accept_entries = False
@app.route("/")
def home():
if basic_auth.authenticate():
return render_template('main_admin.html', list=database.get_list(), auth=basic_auth.authenticate())
else:
return render_template('main.html', list=database.get_list(), auth=basic_auth.authenticate())
@app.route('/api/enqueue', methods=['POST'])
def enqueue():
if accept_entries:
if not request.json:
print(request.data)
abort(400)
name = request.json['name']
song_id = request.json['id']
database.add_entry(name, song_id)
return Response('{"status":"OK"}', mimetype='text/json')
else:
return Response('{"status":"Currently not accepting entries"}', mimetype='text/json',status=423)
@app.route("/list")
def songlist():
return render_template('songlist.html', list=database.get_song_list(), auth=basic_auth.authenticate())
@app.route("/api/queue")
def queue_json():
list = data_adapters.dict_from_rows(database.get_list())
return Response(json.dumps(list, ensure_ascii=False).encode('utf-8'), mimetype='text/json')
@app.route("/plays")
@basic_auth.required
def played_list():
return render_template('played_list.html', list=database.get_played_list(), auth=basic_auth.authenticate())
@app.route("/api/songs")
def songs():
list = database.get_song_list()
return Response(json.dumps(list, ensure_ascii=False).encode('utf-8'), mimetype='text/json')
@app.route("/api/songs/update")
@basic_auth.required
def update_songs():
database.delete_all_entries()
status = database.import_songs(helpers.get_songs(helpers.get_catalog_url()))
print(status)
return Response('{"status": "%s" }' % status, mimetype='text/json')
@app.route("/api/songs/compl")
def get_song_completions(input_string=""):
input_string = request.args.get('search',input_string)
if input_string!="":
print(input_string)
list = database.get_song_completions(input_string=input_string)
return Response(json.dumps(list, ensure_ascii=False).encode('utf-8'), mimetype='text/json')
else:
return 400
@app.route("/api/entries/delete/<entry_id>")
@basic_auth.required
def delete_entry(entry_id):
if database.delete_entry(entry_id):
return Response('{"status": "OK"}', mimetype='text/json')
else:
return Response('{"status": "FAIL"}', mimetype='text/json')
@app.route("/api/entries/delete", methods=['POST'])
@basic_auth.required
def delete_entries():
if not request.json:
print(request.data)
abort(400)
return
updates = database.delete_entries(request.json)
if updates >= 0:
return Response('{"status": "OK", "updates": '+str(updates)+'}', mimetype='text/json')
else:
return Response('{"status": "FAIL"}', mimetype='text/json', status=400)
@app.route("/api/entries/mark_sung/<entry_id>")
@basic_auth.required
def mark_sung(entry_id):
if database.add_sung_song(entry_id):
return Response('{"status": "OK"}', mimetype='text/json')
else:
return Response('{"status": "FAIL"}', mimetype='text/json')
@app.route("/api/entries/accept/<value>")
@basic_auth.required
def set_accept_entries(value):
global accept_entries
if (value=='0' or value=='1'):
accept_entries=bool(int(value))
return Response('{"status": "OK"}', mimetype='text/json')
else:
return Response('{"status": "FAIL"}', mimetype='text/json', status=400)
@app.route("/api/entries/accept")
def get_accept_entries():
global accept_entries
return Response('{"status": "OK", "value": '+str(int(accept_entries))+'}', mimetype='text/json')
@app.route("/api/played/clear")
@basic_auth.required
def clear_played_songs():
if database.clear_played_songs():
return Response('{"status": "OK"}', mimetype='text/json')
else:
return Response('{"status": "FAIL"}', mimetype='text/json')
@app.route("/api/entries/delete_all")
@basic_auth.required
def delete_all_entries():
if database.delete_all_entries():
return Response('{"status": "OK"}', mimetype='text/json')
else:
return Response('{"status": "FAIL"}', mimetype='text/json')
@app.route("/login")
@basic_auth.required
def admin():
return redirect("/", code=303)
@app.before_first_request
def activate_job():
helpers.create_data_directory()
database.create_entry_table()
database.create_song_table()
database.create_done_song_table()
database.create_list_view()
database.create_done_song_view()
helpers.setup_config(app)
if __name__ == "__main__":
app.run(debug=True, host='0.0.0.0')

183
backend/database.py Normal file
View File

@ -0,0 +1,183 @@
# -*- coding: utf_8 -*-
from email.mime import base
from MySQLdb import Connection
from sqlalchemy import create_engine, engine
import pandas
from io import StringIO
from flask import current_app
song_table = "songs"
entry_table = "entries"
index_label = "Id"
done_table = "done_songs"
sql_engine = None
def get_db_engine() -> engine.base.Engine:
global sql_engine
if (not sql_engine):
print(current_app.config.get("DBCONNSTRING"))
sql_engine = create_engine(current_app.config.get("DBCONNSTRING")) # type: ignore
return sql_engine
def import_songs(song_csv):
print("Start importing Songs...")
df = pandas.read_csv(StringIO(song_csv), sep=';')
with get_db_engine().connect() as conn:
df.to_sql(song_table, conn, if_exists='replace',
index=False)
cur = conn.execute("SELECT Count(Id) FROM songs")
num_songs = cur.fetchone()[0] # type: ignore
print("Imported songs ({} in Database)".format(num_songs))
return("Imported songs ({} in Database)".format(num_songs))
def create_entry_table():
with get_db_engine().connect() as conn:
conn.execute('CREATE TABLE IF NOT EXISTS '+entry_table +
' (ID INTEGER PRIMARY KEY NOT NULL AUTO_INCREMENT, Song_Id INTEGER NOT NULL, Name VARCHAR(255), Client_Id VARCHAR(36), Transferred INTEGER DEFAULT 0)')
def create_done_song_table():
with get_db_engine().connect() as conn:
conn.execute('CREATE TABLE IF NOT EXISTS '+done_table +
' (Song_Id INTEGER PRIMARY KEY NOT NULL, Plays INTEGER)')
def create_song_table():
with get_db_engine().connect() as conn:
conn.execute("CREATE TABLE IF NOT EXISTS `"+song_table+"""` (
`Id` INTEGER,
`Title` TEXT,
`Artist` TEXT,
`Year` INTEGER,
`Duo` INTEGER,
`Explicit` INTEGER,
`Date Added` TEXT,
`Styles` TEXT,
`Languages` TEXT
)""")
def create_list_view():
with get_db_engine().connect() as conn:
conn.execute("""CREATE OR REPLACE VIEW `Liste` AS
SELECT Name, Title, Artist, entries.Id AS entry_ID, songs.Id AS song_ID, entries.Transferred
FROM entries, songs
WHERE entries.Song_Id=songs.Id""")
def create_done_song_view():
with get_db_engine().connect() as conn:
conn.execute("""CREATE OR REPLACE VIEW `Abspielliste` AS
SELECT CONCAT(Artist," - ", Title) AS Song, Plays AS Wiedergaben
FROM songs, done_songs
WHERE done_songs.Song_Id=songs.Id""")
def get_list():
with get_db_engine().connect() as conn:
cur = conn.execute("SELECT * FROM Liste")
return cur.fetchall()
def get_played_list():
with get_db_engine().connect() as conn:
cur = conn.execute("SELECT * FROM Abspielliste")
return cur.fetchall()
def get_song_list():
with get_db_engine().connect() as conn:
cur = conn.execute(
"SELECT Artist || \" - \" || Title AS Song, Id FROM songs;")
return cur.fetchall()
def get_song_completions(input_string):
with get_db_engine().connect() as conn:
# Don't look, it burns...
prepared_string = "%{0}%".format(
input_string).upper() # "Test" -> "%TEST%"
print(prepared_string)
cur = conn.execute(
"SELECT CONCAT(Artist,\" - \",Title) AS Song, Id FROM songs WHERE CONCAT(Artist,\" - \",Title) LIKE (%s) LIMIT 20;", [prepared_string])
return cur.fetchall()
def add_entry(name, song_id, client_id):
with get_db_engine().connect() as conn:
conn.execute(
"INSERT INTO entries (Song_Id,Name,Client_Id) VALUES(%s,%s,%s);", (song_id, name, client_id))
return
def add_sung_song(entry_id):
with get_db_engine().connect() as conn:
cur = conn.execute(
"""SELECT Song_Id FROM entries WHERE Id=%s""", (entry_id,))
song_id = cur.fetchone()[0] # type: ignore
conn.execute("""INSERT INTO done_songs (Song_Id, Plays) VALUES("""+str(song_id)+""",1) ON DUPLICATE KEY UPDATE Plays=Plays + 1;""")
delete_entry(entry_id)
return True
def toggle_transferred(entry_id):
with get_db_engine().connect() as conn:
cur = conn.execute(
"SELECT Transferred FROM entries WHERE ID =%s", (entry_id,))
marked = cur.fetchall()[0][0]
if(marked == 0):
conn.execute(
"UPDATE entries SET Transferred = 1 WHERE ID =%s", (entry_id,))
else:
conn.execute(
"UPDATE entries SET Transferred = 0 WHERE ID =%s", (entry_id,))
return True
def check_entry_quota(client_id):
with get_db_engine().connect() as conn:
cur = conn.execute(
"SELECT Count(*) FROM entries WHERE entries.Client_Id = %s", (client_id,))
return cur.fetchall()[0][0]
def check_queue_length():
with get_db_engine().connect() as conn:
cur = conn.execute("SELECT Count(*) FROM entries")
return cur.fetchall()[0][0]
def clear_played_songs():
with get_db_engine().connect() as conn:
conn.execute("DELETE FROM done_songs")
return True
def delete_entry(id):
with get_db_engine().connect() as conn:
conn.execute("DELETE FROM entries WHERE id=%s", (id,))
return True
def delete_entries(ids):
idlist = []
for x in ids:
idlist.append((x,))
try:
with get_db_engine().connect() as conn:
cur = conn.execute("DELETE FROM entries WHERE id=%s", idlist)
return cur.rowcount
except Exception as error:
return -1
def delete_all_entries():
with get_db_engine().connect() as conn:
conn.execute("DELETE FROM entries")
return True

109
backend/helpers.py Normal file
View File

@ -0,0 +1,109 @@
import requests
from bs4 import BeautifulSoup
import json
import os
import uuid
from flask import make_response
from functools import wraps, update_wrapper
from datetime import datetime
data_directory = "data"
config_file = data_directory+"/config.json"
def create_data_directory():
if not os.path.exists(data_directory):
os.makedirs(data_directory)
def get_catalog_url():
r = requests.get('https://www.karafun.de/karaoke-song-list.html')
soup = BeautifulSoup(r.content, 'html.parser')
url = soup.findAll('a', href=True, text='Verfügbar in CSV-Format')[0]['href']
return url
def get_songs(url):
r = requests.get(url)
return r.text
def is_valid_uuid(val):
try:
uuid.UUID(str(val))
return True
except ValueError:
return False
def check_config_exists():
return os.path.isfile(config_file)
def load_version(app):
if os.environ.get("SOURCE_VERSION"):
app.config['VERSION'] = os.environ.get("SOURCE_VERSION")[0:7] # type: ignore
elif os.path.isfile(".version"):
with open('.version', 'r') as file:
data = file.read().replace('\n', '')
if data:
app.config['VERSION'] = data
else:
app.config['VERSION'] = ""
else:
app.config['VERSION'] = ""
def load_dbconfig(app):
if os.environ.get("FLASK_ENV") == "development":
app.config['DBCONNSTRING'] = os.environ.get("DBSTRING")
else:
if os.environ.get("DEPLOYMENT_PLATFORM") == "Heroku":
if os.environ.get("JAWSDB_MARIA_URL"):
app.config['DBCONNSTRING'] = os.environ.get("JAWSDB_MARIA_URL")
else:
app.config['DBCONNSTRING'] = ""
if os.environ.get("DEPLOYMENT_PLATFORM") == "Docker":
if os.environ.get("DBSTRING"):
app.config['DBCONNSTRING'] = os.environ.get("DBSTRING")
else:
app.config['DBCONNSTRING'] = ""
elif os.path.isfile(".dbconn"):
with open('.dbconn', 'r') as file:
data = file.read().replace('\n', '')
if data:
app.config['DBCONNSTRING'] = data
else:
app.config['DBCONNSTRING'] = ""
else:
app.config['DBCONNSTRING'] = ""
def setup_config(app):
if os.environ.get("DEPLOYMENT_PLATFORM") == "Docker":
app.config['BASIC_AUTH_USERNAME'] = os.environ.get('BASIC_AUTH_USERNAME')
app.config['BASIC_AUTH_PASSWORD'] = os.environ.get('BASIC_AUTH_PASSWORD')
app.config['ENTRY_QUOTA'] = os.environ.get('ENTRY_QUOTA')
app.config['MAX_QUEUE'] = os.environ.get('MAX_QUEUE')
else:
if check_config_exists():
config = json.load(open(config_file))
with open(config_file, 'r') as handle:
config = json.load(handle)
print("Loaded existing config")
else:
config = {'username': 'admin', 'password': 'changeme', 'entryquota': 3, 'maxqueue': 20}
with open(config_file, 'w') as handle:
json.dump(config, handle, indent=4, sort_keys=True)
print("Wrote new config")
app.config['BASIC_AUTH_USERNAME'] = config['username']
app.config['BASIC_AUTH_PASSWORD'] = config['password']
app.config['ENTRY_QUOTA'] = config['entryquota']
app.config['MAX_QUEUE'] = config['maxqueue']
def nocache(view):
@wraps(view)
def no_cache(*args, **kwargs):
response = make_response(view(*args, **kwargs))
response.headers['Last-Modified'] = datetime.now() # type: ignore
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '-1'
return response
return update_wrapper(no_cache, view)

4
backend/main.py Normal file
View File

@ -0,0 +1,4 @@
from app import app
if __name__ == "__main__":
app.run()

7
backend/requirements.txt Normal file
View File

@ -0,0 +1,7 @@
requests
pandas
Flask-BasicAuth
bs4
gunicorn
SQLAlchemy
mysqlclient

BIN
backend/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -2,12 +2,13 @@
"name": "KaraoQueue",
"short_name": "KaraoQueue",
"start_url": "/",
"url": "https://karaoqueue-323511.appspot.com/",
"display": "standalone",
"background_color": "#343a40",
"description": "Eine Karaokewarteliste.",
"icons": [{
"src": "images/touch/homescreen192.png",
"sizes": "192x192",
"src": "images/touch/homescreen512.png",
"sizes": "512x512",
"type": "image/png"
}],
"related_applications": [{

View File

@ -12,7 +12,7 @@
<title>{% block title %}{% endblock %} - KaraoQueue</title>
<!-- Bootstrap-Tables -->
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.15.3/dist/bootstrap-table.min.css">
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.21.2/dist/bootstrap-table.min.css">
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
@ -51,6 +51,9 @@
<li class="nav-item">
<a class="nav-link" href="/plays">Abspielliste</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/settings">Einstellungen</a>
</li>
{% endif %}
</ul>
<!--<form class="form-inline my-2 my-lg-0">
@ -68,13 +71,11 @@
<footer class="footer">
<div class="container text-center py-3">
{% if not auth %}
<a href="/login" class="ml-1 mr-1"><i
class="fas fa-sign-in-alt mr-1"></i><span>Login</span></a>
<a href="/login" class="ml-1 mr-1"><i class="fas fa-sign-in-alt mr-1"></i><span>Login</span></a>
{% endif %}
<a href="https://github.com/PhoenixTwoFive/karaoqueue"
class="ml-1 mr-1"><i class="fab fa-github mr-1"></i><span>Github</span></a>
<span class="text-muted">KaraoQueue -&nbsp;<span
style="display:inline-block;transform: rotate(180deg) translateY(-0.2rem)">&copy</span>&nbsp;2019 - Phillip
<span class="text-muted"> {{karaoqueue_version}} -&nbsp;2019-23 - Phillip
Kühne</span>
</div>
</footer>
@ -84,28 +85,46 @@
<!-- Placed at the end of the document so the pages load faster -->
<script src="https://code.jquery.com/jquery-3.2.1.min.js"
integrity="sha384-xBuQ/xzmlsLoJpyjoggmTEz8OWUFM0/RC5BsqQBDX2v5cMvDHcMakNTNrHIW2I5f" crossorigin="anonymous">
</script>
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"
integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous">
</script>
</script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"
integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous">
</script>
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/4.4.0/bootbox.min.js"
integrity="sha256-4F7e4JsAJyLUdpP7Q8Sah866jCOhv72zU5E8lIRER4w=" crossorigin="anonymous">
</script>
<script src="https://unpkg.com/bootstrap-table@1.15.3/dist/bootstrap-table.min.js"></script>
<script src="https://unpkg.com/bootstrap-table@1.15.3/dist/extensions/auto-refresh/bootstrap-table-auto-refresh.min.js"></script>
</script>
<script src="https://unpkg.com/bootstrap-table@1.21.2/dist/bootstrap-table.min.js"></script>
<script
src="https://unpkg.com/bootstrap-table@1.21.2/dist/extensions/auto-refresh/bootstrap-table-auto-refresh.min.js"></script>
<script src="https://gitcdn.github.io/bootstrap-toggle/2.2.2/js/bootstrap-toggle.min.js"></script>
{% block extrajs %}{% endblock %}
<script>
$(document).ready(function () {
loadOrGenerateClientId()
// get current URL path and assign 'active' class
var pathname = window.location.pathname;
$('.navbar-nav > li > a[href="' + pathname + '"]').parent().addClass('active');
$('[data-toggle="tooltip"]').tooltip()
})
function create_UUID() {
var dt = new Date().getTime();
var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = (dt + Math.random() * 16) % 16 | 0;
dt = Math.floor(dt / 16);
return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
return uuid;
}
function loadOrGenerateClientId() {
if (!localStorage.getItem("clientId")) {
localStorage.setItem("clientId",create_UUID())
}
}
</script>
</body>

View File

@ -52,10 +52,10 @@ table td:nth-child(2) {
{% block extrajs %}
<script>
$(function () {
refreshEntryToggle()
$('#entryToggle').change(function() {
$.ajax({url: "/api/entries/accept/"+($('#entryToggle').is(":checked") ? "1" : "0"), complete: setTimeout(refreshEntryToggle, 1000)});
})
refreshEntryToggle()
$("#entrytable").bootstrapTable().on('load-success.bs.table', function() {
$('[data-toggle="tooltip"]').tooltip()
})
@ -116,10 +116,10 @@ table td:nth-child(2) {
$.getJSON("/api/entries/accept", (data) => {
if (data["value"]!=$('#entryToggle').is(":checked")) {
if(data["value"]==1) {
$('#entryToggle').bootstrapToggle('on')
$('#entryToggle').data('bs.toggle').on('true')
}
else {
$('#entryToggle').bootstrapToggle('off')
$('#entryToggle').data('bs.toggle').off('true')
}
}
})
@ -146,6 +146,19 @@ table td:nth-child(2) {
$("#entrytable").bootstrapTable('refresh')
}
function markEntryAsTransferred(entry_id) {
$.ajax({
type: 'GET',
url: '/api/entries/mark_transferred/'+entry_id,
contentType: "application/json",
dataType: 'json',
async: false
});
$("#entrytable").bootstrapTable('refresh')
}
function DeleteSelectedEntries(ids) {
$.ajax({
type: 'POST',
@ -182,11 +195,19 @@ table td:nth-child(2) {
});
}
function TableActions (value, row, index) {
return "<button type=\"button\" class=\"btn btn-success\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Als gesungen markieren\" onclick=\"markEntryAsSung("+row.ID+")\"><i class=\"fas fa-check\"></i></button>&nbsp;<button type=\"button\" class=\"btn btn-danger\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Eintrag löschen\" onclick=\"confirmDeleteEntry('"+row.Name+"',"+row.ID+")\"><i class=\"fas fa-trash\"></i></button>";
console.log("Value: "+value+", Row: "+row+", Index: "+index)
console.log(row)
let outerHTML = ""
if (row.Transferred==1) {
outerHTML = "<button type=\"button\" class=\"btn btn-default\" onclick=\"markEntryAsTransferred("+row.entry_ID+")\"><i class=\"fas fa-backward\"></i></button>&nbsp;<button type=\"button\" class=\"btn btn-success\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Als gesungen markieren\" onclick=\"markEntryAsSung("+row.entry_ID+")\"><i class=\"fas fa-check\"></i></button>&nbsp;<button type=\"button\" class=\"btn btn-danger\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Eintrag löschen\" onclick=\"confirmDeleteEntry('"+row.Name+"',"+row.entry_ID+")\"><i class=\"fas fa-trash\"></i></button>";
} else {
outerHTML = "<button type=\"button\" class=\"btn btn-info\" onclick=\"markEntryAsTransferred("+row.entry_ID+")\"><i class=\"fas fa-exchange-alt\"></i></button>&nbsp;<button type=\"button\" class=\"btn btn-success\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Als gesungen markieren\" onclick=\"markEntryAsSung("+row.entry_ID+")\"><i class=\"fas fa-check\"></i></button>&nbsp;<button type=\"button\" class=\"btn btn-danger\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Eintrag löschen\" onclick=\"confirmDeleteEntry('"+row.Name+"',"+row.entry_ID+")\"><i class=\"fas fa-trash\"></i></button>";
}
return outerHTML;
}
function getIdSelections() {
return $.map($("#entrytable").bootstrapTable('getSelections'), function (row) {
return row.ID
return row.entry_ID
})
}
</script>

View File

@ -38,8 +38,8 @@
</table>
{% endblock %}
{% block extrajs %}
<script src="https://unpkg.com/jspdf@latest/dist/jspdf.min.js"></script>
<script src="https://unpkg.com/jspdf-autotable@3.1.1/dist/jspdf.plugin.autotable.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.5.3/jspdf.min.js"></script>
<script src="https://unpkg.com/jspdf-autotable@3.0.10/dist/jspdf.plugin.autotable.js"></script>
<script>
function confirmDeleteAllEntries() {
bootbox.confirm({

View File

@ -0,0 +1,18 @@
{% extends 'base.html' %}
{% block title %}Einstellungen{% endblock %}
{% block content %}
<form method="post">
<p>
<label for="entryquota">Maximale Anzahl an Einträgen pro Nutzer</label>
<input type="number" class="form-control" id="entryquota" name="entryquota" min=1 value={{app.config['ENTRY_QUOTA']}}>
</p>
<p>
<label for="maxqueue">Maximale Anzahl an Einträgen Insgesamt</label>
<input type="number" class="form-control" id="maxqueue" name="maxqueue" min=1 value={{app.config['MAX_QUEUE']}}>
</p>
<input type="submit" class="btn btn-primary mr-1 mb-2" value="Einstellungen anwenden">
<input type="button" class="btn btn-default mr-1 mb-2" onclick="$.get('/writeSettings').done(()=>{alert('Einstellungen gespeichert')}).fail(()=>{alert('Fehler beim Speichern der Einstellungen')})" value="Einstellungen speichern"/>
</form>
{% endblock %}
{% block extrajs %}
{% endblock %}

View File

@ -17,8 +17,8 @@
</button>
</div>
<div class="form-group">
<form id="nameForm">
<div class="modal-body">
<form id="nameForm">
<div class="modal-body">
<label for="singerNameInput">Sängername</label>
<input type="text" class="form-control" id="singerNameInput" placeholder="Max Mustermann"
required>
@ -28,7 +28,7 @@
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary" id="submitSongButton">Anmelden</button>
</div>
</form>
</form>
</div>
</div>
</div>
@ -38,18 +38,18 @@
<script>
$(document).ready(function () {
$("#filter").focus();
$("#filter").keyup( function () {
$("#filter").keyup(function () {
var value = $(this).val().toLowerCase();
//alert(value);
if(value.length >= 1) {
if (value.length >= 1) {
$.getJSON("/api/songs/compl", { search: value }, function (data) {
var items = [];
$.each(data, function (key, val) {
items.push("<tr><td>"+val[0]+`</td>
items.push("<tr><td>" + val[0] + `</td>
<td><button type='button'
class='btn btn-primary justify-content-center align-content-between enqueueButton'
data-toggle='modal'
data-target='#enqueueModal' onclick='setSelectedId(`+val[1]+`)'><i
data-target='#enqueueModal' onclick='setSelectedId(`+ val[1] + `)'><i
class="fas fa-plus"></i></button></td>
</tr>`)
});
@ -63,17 +63,18 @@
}
});
$("#nameForm").submit( function (e) {
$("#nameForm").submit(function (e) {
e.preventDefault();
submitModal();
});
});
function enqueue(id,name,success_callback, blocked_callback) {
function enqueue(client_id, id, name, success_callback, blocked_callback) {
var data = {
"name": name,
"id": id
"id": id,
"client_id": client_id
}
$.ajax({
type: 'POST',
@ -89,20 +90,20 @@
}
function setSelectedId(id) {
$("#selectedId").attr("value",id);
$("#selectedId").attr("value", id);
}
function submitModal() {
var name = $("#singerNameInput").val();
var id = $("#selectedId").attr("value");
enqueue(id,name,function () {
enqueue(localStorage.getItem("clientId"),id, name, function () {
$("#enqueueModal").modal('hide');
window.location.href = '/#end';
}, function () {
}, function (response) {
bootbox.alert({
message: "Es werden leider keine neuen Anmeldungen mehr angenommen. Tut mir leid :(",
});
$(".enqueueButton").prop("disabled",true);
message: "Deine Eintragung konnte leider nicht vorgenommen werden.\nGrund: "+response.responseJSON.status,
});
entriesAccepted();
$("#enqueueModal").modal('hide');
});
@ -110,20 +111,26 @@
}
{% if not auth %}
function entriesAccepted() {
$.getJSON("/api/entries/accept", (data,out) => {
if(data["value"]==0) {
$(".enqueueButton").prop("disabled",true)
$(".enqueueButton").prop("style","pointer-events: none;")
$(".enqueueButton").wrap("<span class='tooltip-span' tabindex='0' data-toggle='tooltip' data-placement='top'></span>" );
$(".tooltip-span").prop("title","Eintragungen sind leider nicht mehr möglich.")
$.getJSON("/api/entries/accept", (data, out) => {
if (data["value"] == 0) {
$(".enqueueButton").prop("disabled", true)
$(".enqueueButton").prop("style", "pointer-events: none;")
$(".enqueueButton").wrap("<span class='tooltip-span' tabindex='0' data-toggle='tooltip' data-placement='top'></span>");
$(".tooltip-span").prop("title", "Eintragungen sind leider nicht mehr möglich.")
$('[data-toggle="tooltip"]').tooltip()
} else {
$(".enqueueButton").prop("disabled",false)
$(".enqueueButton").prop("disabled", false)
}
})
}
{% else %}
function entriesAccepted() {
$(".enqueueButton").prop("disabled", false)
}
{% endif %}
</script>
{% endblock %}

28
docker-compose.prod.yml Normal file
View File

@ -0,0 +1,28 @@
version: "3.9"
secrets:
secrets:
file: ./secrets.yml
services:
karaoqueue:
image: "phillipkhne/karaoqueue:latest"
restart: always
ports:
- "127.0.0.1:8081:80" # Please put a reverse proxy in front of this
environment:
DEPLOYMENT_PLATFORM: Docker
DBSTRING: mysql://user:pass@host:3306/database
BASIC_AUTH_USERNAME: admin
BASIC_AUTH_PASSWORD: changeme
ENTRY_QUOTA: 3
MAX_QUEUE: 20
db:
image: mariadb
restart: always
environment:
MARIADB_ROOT_PASSWORD: changeme!
MARIADB_ROOT_HOST: localhost
MARIADB_DATABASE: karaoqueue
MARIADB_USER: karaoqueue
MARIADB_PASSWORD: change

15
docker-compose.yml Normal file
View File

@ -0,0 +1,15 @@
# This Compose file is for development only. It is not intended for production use.
# It only starts auxiliary services, such as a database, that are required for the
# application to run. The application itself is started separately, using the
# command "python -m flask run" or your favorite IDE.
# Useful for attaching a debugger to the application.
version: "3.9"
services:
db:
image: mariadb
restart: always
env_file: .env.dev
ports:
- "3306:3306"