Compare commits

...

42 Commits

Author SHA1 Message Date
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
27 changed files with 512 additions and 274 deletions

3
.gitignore vendored
View File

@ -135,3 +135,6 @@ data/
# Node Modules # Node Modules
node_modules/ node_modules/
# Version identification file
.version

57
.vscode/launch.json vendored
View File

@ -4,35 +4,72 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "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", "type": "python",
"cwd": "${workspaceFolder}/backend",
"request": "launch", "request": "launch",
"module": "flask", "module": "flask",
"env": { "env": {
"FLASK_APP": "backend/app/main.py", "FLASK_APP": "backend/app.py",
"FLASK_ENV": "development",
"FLASK_DEBUG": "1",
"DBSTRING": "mysql://devuser:devpw@127.0.0.1:3306/karaoqueue"
},
"args": [
"run",
"--no-debugger",
"--no-reload"
],
"jinja": true
},
{
"preLaunchTask": "mariadb",
"name": "Python: Flask (with reload)",
"type": "python",
"cwd": "${workspaceFolder}/backend",
"request": "launch",
"module": "flask",
"env": {
"FLASK_APP": "backend/app.py",
"FLASK_ENV": "development",
"FLASK_DEBUG": "1"
},
"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_ENV": "development",
"FLASK_DEBUG": "1" "FLASK_DEBUG": "1"
}, },
"args": [ "args": [
"run", "run",
"--no-debugger", "--no-debugger",
"--host='0.0.0.0'" "--host=0.0.0.0"
], ],
"jinja": true "jinja": true
}, },
{ {
"preLaunchTask": "mariadb",
"name": "Python: Flask (externally reachable)", "name": "Python: Flask (externally reachable)",
"type": "python", "type": "python",
"cwd": "${workspaceFolder}/backend",
"request": "launch", "request": "launch",
"module": "flask", "module": "flask",
"env": { "env": {
"FLASK_APP": "backend/app/main.py", "FLASK_APP": "backend/app.py",
"FLASK_ENV": "development", "FLASK_ENV": "development",
"FLASK_DEBUG": "1" "FLASK_DEBUG": "1"
}, },
@ -40,7 +77,7 @@
"run", "run",
"--no-debugger", "--no-debugger",
"--no-reload", "--no-reload",
"--host='0.0.0.0'" "--host=0.0.0.0"
], ],
"jinja": true "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 run --rm --name some-mariadb --env MARIADB_USER=devuser --env MARIADB_PASSWORD=devpw --env MARIADB_ROOT_PASSWORD=devrootpw --env MARIADB_DATABASE=karaoqueue -p 3306:3306 mariadb:latest",
"isBackground": true,
"activeOnStart": false
}
]
}

View File

@ -8,4 +8,4 @@ RUN pip install Flask-BasicAuth
RUN pip install bs4 RUN pip install bs4
COPY ./app /app COPY ./backend /app

1
backend/Procfile Normal file
View File

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

View File

@ -1,16 +1,16 @@
from flask import Flask, render_template, Response, abort, request, redirect from flask import Flask, render_template, abort, request, redirect, send_from_directory, jsonify
from flask.wrappers import Request, Response
import helpers import helpers
import database import database
import data_adapters import data_adapters
import os import os
import errno
import json import json
from flask_basicauth import BasicAuth from flask_basicauth import BasicAuth
from helpers import nocache
app = Flask(__name__, static_url_path='/static') app = Flask(__name__, static_url_path='/static')
basic_auth = BasicAuth(app) basic_auth = BasicAuth(app)
accept_entries = False accept_entries = True
@app.route("/") @app.route("/")
def home(): def home():
@ -20,7 +20,14 @@ def home():
return render_template('main.html', list=database.get_list(), auth=basic_auth.authenticate()) 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']) @app.route('/api/enqueue', methods=['POST'])
@nocache
def enqueue(): def enqueue():
if not request.json: if not request.json:
print(request.data) print(request.data)
@ -63,22 +70,24 @@ def songlist():
@app.route("/settings") @app.route("/settings")
@nocache
@basic_auth.required @basic_auth.required
def settings(): def settings():
return render_template('settings.html', app=app, auth=basic_auth.authenticate()) return render_template('settings.html', app=app, auth=basic_auth.authenticate())
@app.route("/settings", methods=['POST']) @app.route("/settings", methods=['POST'])
@nocache
@basic_auth.required @basic_auth.required
def settings_post(): def settings_post():
entryquota = request.form.get("entryquota") entryquota = request.form.get("entryquota")
maxqueue = request.form.get("maxqueue") maxqueue = request.form.get("maxqueue")
if entryquota.isnumeric() and int(entryquota) > 0: if entryquota.isnumeric() and int(entryquota) > 0: # type: ignore
app.config['ENTRY_QUOTA'] = int(entryquota) app.config['ENTRY_QUOTA'] = int(entryquota) # type: ignore
else: else:
abort(400) abort(400)
if maxqueue.isnumeric and int(maxqueue) > 0: if maxqueue.isnumeric and int(maxqueue) > 0: # type: ignore
app.config['MAX_QUEUE'] = int(maxqueue) app.config['MAX_QUEUE'] = int(maxqueue) # type: ignore
else: else:
abort(400) abort(400)
@ -86,24 +95,28 @@ def settings_post():
@app.route("/api/queue") @app.route("/api/queue")
@nocache
def queue_json(): def queue_json():
list = data_adapters.dict_from_rows(database.get_list()) list = data_adapters.dict_from_rows(database.get_list())
return Response(json.dumps(list, ensure_ascii=False).encode('utf-8'), mimetype='text/json') return Response(json.dumps(list, ensure_ascii=False).encode('utf-8'), mimetype='text/json')
@app.route("/plays") @app.route("/plays")
@nocache
@basic_auth.required @basic_auth.required
def played_list(): def played_list():
return render_template('played_list.html', list=database.get_played_list(), auth=basic_auth.authenticate()) return render_template('played_list.html', list=database.get_played_list(), auth=basic_auth.authenticate())
@app.route("/api/songs") @app.route("/api/songs")
@nocache
def songs(): def songs():
list = database.get_song_list() list = database.get_song_list()
return Response(json.dumps(list, ensure_ascii=False).encode('utf-8'), mimetype='text/json') return Response(json.dumps(list, ensure_ascii=False).encode('utf-8'), mimetype='text/json')
@app.route("/api/songs/update") @app.route("/api/songs/update")
@nocache
@basic_auth.required @basic_auth.required
def update_songs(): def update_songs():
database.delete_all_entries() database.delete_all_entries()
@ -114,18 +127,20 @@ def update_songs():
@app.route("/api/songs/compl") @app.route("/api/songs/compl")
@nocache
def get_song_completions(input_string=""): def get_song_completions(input_string=""):
input_string = request.args.get('search', input_string) input_string = request.args.get('search', input_string)
if input_string != "": if input_string != "":
print(input_string) print(input_string)
list = database.get_song_completions(input_string=input_string) result = [list(x) for x in database.get_song_completions(input_string=input_string)]
return Response(json.dumps(list, ensure_ascii=False).encode('utf-8'), mimetype='text/json') return jsonify(result)
else: else:
return 400 return 400
@app.route("/api/entries/delete/<entry_id>") @app.route("/api/entries/delete/<entry_id>")
@nocache
@basic_auth.required @basic_auth.required
def delete_entry(entry_id): def delete_entry(entry_id):
if database.delete_entry(entry_id): if database.delete_entry(entry_id):
@ -135,6 +150,7 @@ def delete_entry(entry_id):
@app.route("/api/entries/delete", methods=['POST']) @app.route("/api/entries/delete", methods=['POST'])
@nocache
@basic_auth.required @basic_auth.required
def delete_entries(): def delete_entries():
if not request.json: if not request.json:
@ -149,6 +165,7 @@ def delete_entries():
@app.route("/api/entries/mark_sung/<entry_id>") @app.route("/api/entries/mark_sung/<entry_id>")
@nocache
@basic_auth.required @basic_auth.required
def mark_sung(entry_id): def mark_sung(entry_id):
if database.add_sung_song(entry_id): if database.add_sung_song(entry_id):
@ -156,8 +173,18 @@ def mark_sung(entry_id):
else: else:
return Response('{"status": "FAIL"}', mimetype='text/json') 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>") @app.route("/api/entries/accept/<value>")
@nocache
@basic_auth.required @basic_auth.required
def set_accept_entries(value): def set_accept_entries(value):
global accept_entries global accept_entries
@ -169,12 +196,14 @@ def set_accept_entries(value):
@app.route("/api/entries/accept") @app.route("/api/entries/accept")
@nocache
def get_accept_entries(): def get_accept_entries():
global accept_entries global accept_entries
return Response('{"status": "OK", "value": '+str(int(accept_entries))+'}', mimetype='text/json') return Response('{"status": "OK", "value": '+str(int(accept_entries))+'}', mimetype='text/json')
@app.route("/api/played/clear") @app.route("/api/played/clear")
@nocache
@basic_auth.required @basic_auth.required
def clear_played_songs(): def clear_played_songs():
if database.clear_played_songs(): if database.clear_played_songs():
@ -184,6 +213,7 @@ def clear_played_songs():
@app.route("/api/entries/delete_all") @app.route("/api/entries/delete_all")
@nocache
@basic_auth.required @basic_auth.required
def delete_all_entries(): def delete_all_entries():
if database.delete_all_entries(): if database.delete_all_entries():
@ -200,6 +230,8 @@ def admin():
@app.before_first_request @app.before_first_request
def activate_job(): def activate_job():
helpers.load_dbconfig(app)
helpers.load_version(app)
helpers.create_data_directory() helpers.create_data_directory()
database.create_entry_table() database.create_entry_table()
database.create_song_table() database.create_song_table()
@ -209,5 +241,21 @@ def activate_job():
helpers.setup_config(app) 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__": if __name__ == "__main__":
app.run(host='127.0.0.1', port=8080, debug=True) app.run(host='127.0.0.1', port=8080, debug=True)

View File

@ -1,193 +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("/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=';')
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), Client_Id VARCHAR(36))')
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, client_id):
conn = open_db()
cur = conn.cursor()
cur.execute(
"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,))
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 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()
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,49 +0,0 @@
import requests
from bs4 import BeautifulSoup
import json
import os
import uuid
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 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 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', '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']

View File

@ -1,4 +0,0 @@
requests
pandas
Flask-BasicAuth
bs4

204
backend/database.py Normal file
View File

@ -0,0 +1,204 @@
# -*- 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"
connection = None
def open_db() -> engine.base.Connection:
global connection
if (not connection):
print(current_app.config.get("DBCONNSTRING"))
engine = create_engine(current_app.config.get("DBCONNSTRING")) # type: ignore
connection = engine.connect()
# cur.execute('PRAGMA encoding = "UTF-8";')
return connection
def import_songs(song_csv):
print("Start importing Songs...")
df = pandas.read_csv(StringIO(song_csv), sep=';')
conn = open_db()
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
# 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 AUTO_INCREMENT, Song_Id INTEGER NOT NULL, Name VARCHAR(255), Client_Id VARCHAR(36), Transferred INTEGER DEFAULT 0)')
# 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 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""")
# conn.close()
def create_done_song_view():
conn = open_db()
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""")
# conn.close()
def get_list():
conn = open_db()
cur = conn.execute("SELECT * FROM Liste")
return cur.fetchall()
def get_played_list():
conn = open_db()
cur = conn.execute("SELECT * FROM Abspielliste")
return cur.fetchall()
def get_song_list():
conn = open_db()
cur = conn.execute(
"SELECT Artist || \" - \" || Title AS Song, Id FROM songs;")
return cur.fetchall()
def get_song_completions(input_string):
conn = open_db()
# 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):
conn = open_db()
conn.execute(
"INSERT INTO entries (Song_Id,Name,Client_Id) VALUES(%s,%s,%s);", (song_id, name, client_id))
# conn.close()
return
def add_sung_song(entry_id):
conn = open_db()
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;""")
# SQLite bullshittery
# conn.execute("""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)"
# )
delete_entry(entry_id)
# conn.close()
return True
def toggle_transferred(entry_id):
conn = open_db()
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,))
# conn.close()
return True
def check_entry_quota(client_id):
conn = open_db()
cur = conn.execute(
"SELECT Count(*) FROM entries WHERE entries.Client_Id = %s", (client_id,))
return cur.fetchall()[0][0]
def check_queue_length():
conn = open_db()
cur = conn.execute("SELECT Count(*) FROM entries")
return cur.fetchall()[0][0]
def clear_played_songs():
conn = open_db()
conn.execute("DELETE FROM done_songs")
# conn.close()
return True
def delete_entry(id):
conn = open_db()
conn.execute("DELETE FROM entries WHERE id=%s", (id,))
# conn.close()
return True
def delete_entries(ids):
idlist = []
for x in ids:
idlist.append((x,))
try:
conn = open_db()
cur = conn.execute("DELETE FROM entries WHERE id=%s", idlist)
# conn.close()
return cur.rowcount
except Exception as error:
return -1
def delete_all_entries():
conn = open_db()
conn.execute("DELETE FROM entries")
# conn.close()
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)

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", "name": "KaraoQueue",
"short_name": "KaraoQueue", "short_name": "KaraoQueue",
"start_url": "/", "start_url": "/",
"url": "https://karaoqueue-323511.appspot.com/",
"display": "standalone", "display": "standalone",
"background_color": "#343a40", "background_color": "#343a40",
"description": "Eine Karaokewarteliste.", "description": "Eine Karaokewarteliste.",
"icons": [{ "icons": [{
"src": "images/touch/homescreen192.png", "src": "images/touch/homescreen512.png",
"sizes": "192x192", "sizes": "512x512",
"type": "image/png" "type": "image/png"
}], }],
"related_applications": [{ "related_applications": [{

View File

@ -75,7 +75,7 @@
{% endif %} {% endif %}
<!--<a href="https://github.com/PhoenixTwoFive/karaoqueue" <!--<a href="https://github.com/PhoenixTwoFive/karaoqueue"
class="ml-1 mr-1"><i class="fab fa-github mr-1"></i><span>Github</span></a>--> class="ml-1 mr-1"><i class="fab fa-github mr-1"></i><span>Github</span></a>-->
<span class="text-muted">KaraoQueue (stale branch) -&nbsp;<span>&copy</span>&nbsp;2019-2021 - Phillip <span class="text-muted"><a class="text-muted" href="https://github.com/PhoenixTwoFive/karaoqueue">KaraoQueue</a> {{karaoqueue_version}} -&nbsp;2019-22 - Phillip
Kühne</span> Kühne</span>
</div> </div>
</footer> </footer>

View File

@ -52,10 +52,10 @@ table td:nth-child(2) {
{% block extrajs %} {% block extrajs %}
<script> <script>
$(function () { $(function () {
refreshEntryToggle()
$('#entryToggle').change(function() { $('#entryToggle').change(function() {
$.ajax({url: "/api/entries/accept/"+($('#entryToggle').is(":checked") ? "1" : "0"), complete: setTimeout(refreshEntryToggle, 1000)}); $.ajax({url: "/api/entries/accept/"+($('#entryToggle').is(":checked") ? "1" : "0"), complete: setTimeout(refreshEntryToggle, 1000)});
}) })
refreshEntryToggle()
$("#entrytable").bootstrapTable().on('load-success.bs.table', function() { $("#entrytable").bootstrapTable().on('load-success.bs.table', function() {
$('[data-toggle="tooltip"]').tooltip() $('[data-toggle="tooltip"]').tooltip()
}) })
@ -146,6 +146,19 @@ table td:nth-child(2) {
$("#entrytable").bootstrapTable('refresh') $("#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) { function DeleteSelectedEntries(ids) {
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
@ -182,11 +195,19 @@ table td:nth-child(2) {
}); });
} }
function TableActions (value, row, index) { 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() { function getIdSelections() {
return $.map($("#entrytable").bootstrapTable('getSelections'), function (row) { return $.map($("#entrytable").bootstrapTable('getSelections'), function (row) {
return row.ID return row.entry_ID
}) })
} }
</script> </script>

View File

@ -111,6 +111,7 @@
} }
{% if not auth %}
function entriesAccepted() { function entriesAccepted() {
$.getJSON("/api/entries/accept", (data, out) => { $.getJSON("/api/entries/accept", (data, out) => {
if (data["value"] == 0) { if (data["value"] == 0) {
@ -125,6 +126,11 @@
}) })
} }
{% else %}
function entriesAccepted() {
$(".enqueueButton").prop("disabled", false)
}
{% endif %}
</script> </script>
{% endblock %} {% endblock %}

4
backend/wsgi.py Normal file
View File

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

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

@ -0,0 +1,23 @@
version: "3.9"
services:
karaoqueue:
image: "phillipkhne/karaoqueue:latest"
restart: always
ports:
- "127.0.0.1:8081:80"
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: dpMAZj*Mc4%FZM!V
MARIADB_ROOT_HOST: localhost
MARIADB_DATABASE: karaoqueue
MARIADB_USER: karaoqueue
MARIADB_PASSWORD: a5G@P*^tCW$$w@wE