diff --git a/backend/app/database.py b/backend/app/database.py index a6d36f8..4cae8a2 100644 --- a/backend/app/database.py +++ b/backend/app/database.py @@ -4,16 +4,18 @@ import sqlite3 import pandas from io import StringIO -song_table = "songs" +song_table = "songs" entry_table = "entries" index_label = "Id" -done_table = "done_songs" +done_table = "done_songs" + def open_db(): conn = sqlite3.connect("/tmp/karaoqueue.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=';') @@ -25,12 +27,13 @@ def import_songs(song_csv): num_songs = cur.fetchone()[0] conn.close() print("Imported songs ({} in Database)".format(num_songs)) - return("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))') + ' (ID INTEGER PRIMARY KEY NOT NULL, Song_Id INTEGER NOT NULL, Name VARCHAR(255), Client_Id VARCHAR(36))') conn.close() @@ -40,6 +43,7 @@ def create_done_song_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+"""\" ( @@ -55,6 +59,7 @@ def create_song_table(): )""") conn.close() + def create_list_view(): conn = open_db() conn.execute("""CREATE VIEW IF NOT EXISTS [Liste] AS @@ -72,6 +77,7 @@ def create_done_song_view(): WHERE done_songs.Song_Id=songs.Id""") conn.close() + def get_list(): conn = open_db() conn.row_factory = sqlite3.Row @@ -86,35 +92,40 @@ def get_played_list(): cur.execute("SELECT * FROM Abspielliste") return cur.fetchall() + def get_song_list(): - conn =open_db() + 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%" + 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): + +def add_entry(name, song_id, client_id): conn = open_db() cur = conn.cursor() cur.execute( - "INSERT INTO entries (Song_Id,Name) VALUES(?,?);", (song_id,name)) + "INSERT INTO entries (Song_Id,Name,Client_Id) VALUES(?,?,?);", (song_id, name, client_id)) 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,)) + 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)+""", @@ -127,6 +138,19 @@ def add_sung_song(entry_id): conn.close() return True + +def check_entry_quota(client_id): + conn = open_db() + cur = conn.cursor() + cur.execute("SELECT Count(*) FROM entries WHERE entries.Client_Id = ?", (client_id,)) + return cur.fetchall()[0][0] + +def check_queue_length(): + conn = open_db() + cur = conn.cursor() + cur.execute("SELECT Count(*) FROM entries") + return cur.fetchall()[0][0] + def clear_played_songs(): conn = open_db() cur = conn.cursor() @@ -135,10 +159,11 @@ def clear_played_songs(): conn.close() return True + def delete_entry(id): conn = open_db() cur = conn.cursor() - cur.execute("DELETE FROM entries WHERE id=?",(id,)) + cur.execute("DELETE FROM entries WHERE id=?", (id,)) conn.commit() conn.close() return True @@ -147,7 +172,7 @@ def delete_entry(id): def delete_entries(ids): idlist = [] for x in ids: - idlist.append( (x,) ) + idlist.append((x,)) try: conn = open_db() cur = conn.cursor() @@ -158,6 +183,7 @@ def delete_entries(ids): except sqlite3.Error as error: return -1 + def delete_all_entries(): conn = open_db() cur = conn.cursor() diff --git a/backend/app/helpers.py b/backend/app/helpers.py index fe932fb..4261d17 100644 --- a/backend/app/helpers.py +++ b/backend/app/helpers.py @@ -2,6 +2,7 @@ import requests from bs4 import BeautifulSoup import json import os +import uuid data_directory = "data" config_file = data_directory+"/config.json" @@ -21,6 +22,13 @@ 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) @@ -31,9 +39,11 @@ def setup_config(app): config = json.load(handle) print("Loaded existing config") else: - config = {'username': 'admin', 'password': 'changeme'} + 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'] \ No newline at end of file + app.config['BASIC_AUTH_PASSWORD'] = config['password'] + app.config['ENTRY_QUOTA'] = config['entryquota'] + app.config['MAX_QUEUE'] = config['maxqueue'] \ No newline at end of file diff --git a/backend/app/main.py b/backend/app/main.py index cad59ac..570c7f6 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -2,7 +2,8 @@ from flask import Flask, render_template, Response, abort, request, redirect import helpers import database import data_adapters -import os, errno +import os +import errno import json from flask_basicauth import BasicAuth app = Flask(__name__, static_url_path='/static') @@ -10,6 +11,7 @@ app = Flask(__name__, static_url_path='/static') basic_auth = BasicAuth(app) accept_entries = False + @app.route("/") def home(): if basic_auth.authenticate(): @@ -17,52 +19,104 @@ def home(): 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) + 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: - return Response('{"status":"Currently not accepting entries"}', mimetype='text/json',status=423) - + 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() < app.config['MAX_QUEUE']: + if database.check_entry_quota(client_id) < 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") +@basic_auth.required +def settings(): + return render_template('settings.html', app=app, auth=basic_auth.authenticate()) + + +@app.route("/settings", methods=['POST']) +@basic_auth.required +def settings_post(): + entryquota = request.form.get("entryquota") + maxqueue = request.form.get("maxqueue") + if entryquota.isnumeric() and int(entryquota) > 0: + app.config['ENTRY_QUOTA'] = int(entryquota) + else: + abort(400) + if maxqueue.isnumeric and int(maxqueue) > 0: + app.config['MAX_QUEUE'] = int(maxqueue) + else: + abort(400) + + return render_template('settings.html', app=app, 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())) + 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!="": + 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') @@ -102,12 +156,13 @@ def mark_sung(entry_id): else: return Response('{"status": "FAIL"}', mimetype='text/json') + @app.route("/api/entries/accept/") @basic_auth.required def set_accept_entries(value): global accept_entries - if (value=='0' or value=='1'): - accept_entries=bool(int(value)) + 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) @@ -117,7 +172,7 @@ def set_accept_entries(value): 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 @@ -127,6 +182,7 @@ def clear_played_songs(): else: return Response('{"status": "FAIL"}', mimetype='text/json') + @app.route("/api/entries/delete_all") @basic_auth.required def delete_all_entries(): @@ -135,11 +191,13 @@ def delete_all_entries(): 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() @@ -151,5 +209,5 @@ def activate_job(): helpers.setup_config(app) -if __name__ == "__main__": +if __name__ == "__main__": app.run(host='127.0.0.1', port=8080, debug=True) diff --git a/backend/app/templates/base.html b/backend/app/templates/base.html index b51155c..934135e 100644 --- a/backend/app/templates/base.html +++ b/backend/app/templates/base.html @@ -10,7 +10,7 @@ {% block title %}{% endblock %} - KaraoQueue - + @@ -51,6 +51,9 @@ + {% endif %} @@ -83,28 +85,46 @@ - + + + + + - + {% block extrajs %}{% endblock %} diff --git a/backend/app/templates/main_admin.html b/backend/app/templates/main_admin.html index 1006acf..42d40f8 100644 --- a/backend/app/templates/main_admin.html +++ b/backend/app/templates/main_admin.html @@ -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') } } }) diff --git a/backend/app/templates/settings.html b/backend/app/templates/settings.html new file mode 100644 index 0000000..6678bbf --- /dev/null +++ b/backend/app/templates/settings.html @@ -0,0 +1,18 @@ +{% extends 'base.html' %} +{% block title %}Einstellungen{% endblock %} +{% block content %} +
+

+ + +

+

+ + +

+ + +
+{% endblock %} +{% block extrajs %} +{% endblock %} \ No newline at end of file diff --git a/backend/app/templates/songlist.html b/backend/app/templates/songlist.html index c7791b1..5294024 100644 --- a/backend/app/templates/songlist.html +++ b/backend/app/templates/songlist.html @@ -17,8 +17,8 @@
-
-
@@ -38,22 +38,22 @@