Compare commits

...

40 Commits

Author SHA1 Message Date
79156c539a Better Dark Mode 2023-03-30 04:43:53 +02:00
757bfa2483 Fix Dark Mode 2023-03-30 03:28:23 +02:00
d56eb609b9 Fix entry button alignment 2023-03-30 03:25:02 +02:00
6d9541f0bd Add Dark Mode 2023-03-30 03:24:45 +02:00
ee4774159c Update Python to 3.10 2023-03-30 03:23:22 +02:00
6d084ee83c Add theming 2023-03-30 02:57:21 +02:00
971548189f Remove redundant save button from settings 2023-03-30 02:26:24 +02:00
da8ad57293 Fix Settings 2023-03-30 02:22:59 +02:00
98981b1e1e Move config to database 2023-03-30 02:22:46 +02:00
d33006251e Remove inline env from docker-compose.prod.yml 2023-03-30 02:22:02 +02:00
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
20 changed files with 537 additions and 183 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

3
.gitignore vendored
View File

@ -138,3 +138,6 @@ node_modules/
# Version identification file
.version
# Docker secrets
secrets.yml

14
.vscode/launch.json vendored
View File

@ -5,7 +5,7 @@
"version": "0.2.0",
"configurations": [
{
"preLaunchTask": "versiondump",
"preLaunchTask": "mariadb",
"name": "Python: Flask",
"type": "python",
"cwd": "${workspaceFolder}/backend",
@ -14,8 +14,9 @@
"env": {
"FLASK_APP": "backend/app.py",
"FLASK_ENV": "development",
"FLASK_DEBUG": "1"
"FLASK_DEBUG": "1",
},
"envFile": "${workspaceFolder}/.env.dev",
"args": [
"run",
"--no-debugger",
@ -24,7 +25,7 @@
"jinja": true
},
{
"preLaunchTask": "versiondump",
"preLaunchTask": "mariadb",
"name": "Python: Flask (with reload)",
"type": "python",
"cwd": "${workspaceFolder}/backend",
@ -35,6 +36,7 @@
"FLASK_ENV": "development",
"FLASK_DEBUG": "1"
},
"envFile": "${workspaceFolder}/.env.dev",
"args": [
"run",
"--no-debugger"
@ -42,7 +44,7 @@
"jinja": true
},
{
"preLaunchTask": "versiondump",
"preLaunchTask": "mariadb",
"name": "Python: Flask (with reload, externally reachable)",
"type": "python",
"cwd": "${workspaceFolder}/backend",
@ -53,6 +55,7 @@
"FLASK_ENV": "development",
"FLASK_DEBUG": "1"
},
"envFile": "${workspaceFolder}/.env.dev",
"args": [
"run",
"--no-debugger",
@ -61,7 +64,7 @@
"jinja": true
},
{
"preLaunchTask": "versiondump",
"preLaunchTask": "mariadb",
"name": "Python: Flask (externally reachable)",
"type": "python",
"cwd": "${workspaceFolder}/backend",
@ -72,6 +75,7 @@
"FLASK_ENV": "development",
"FLASK_DEBUG": "1"
},
"envFile": "${workspaceFolder}/.env.dev",
"args": [
"run",
"--no-debugger",

7
.vscode/tasks.json vendored
View File

@ -8,6 +8,13 @@
"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.10
COPY ./backend /app
RUN pip install -r /app/requirements.txt

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

View File

@ -1,4 +1,5 @@
from flask import Flask, render_template, Response, abort, request, redirect, send_from_directory
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
@ -9,7 +10,7 @@ from helpers import nocache
app = Flask(__name__, static_url_path='/static')
basic_auth = BasicAuth(app)
accept_entries = False
accept_entries = True
@app.route("/")
def home():
@ -41,7 +42,7 @@ def enqueue():
database.add_entry(name, song_id, client_id)
return Response('{"status":"OK"}', mimetype='text/json')
else:
if accept_entries:
if helpers.get_accept_entries(app):
if not request.json:
print(request.data)
abort(400)
@ -51,8 +52,8 @@ def enqueue():
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']:
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:
@ -72,7 +73,7 @@ def songlist():
@nocache
@basic_auth.required
def settings():
return render_template('settings.html', app=app, auth=basic_auth.authenticate())
return render_template('settings.html', app=app, auth=basic_auth.authenticate(), themes=helpers.get_themes())
@app.route("/settings", methods=['POST'])
@ -81,14 +82,21 @@ def settings():
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)
theme = request.form.get("theme")
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:
app.config['MAX_QUEUE'] = int(maxqueue)
if maxqueue.isnumeric and int(maxqueue) > 0: # type: ignore
app.config['MAX_QUEUE'] = int(maxqueue) # type: ignore
else:
abort(400)
if theme in helpers.get_themes():
app.config['THEME'] = theme
else:
abort(400)
helpers.persist_config(app=app)
return render_template('settings.html', app=app, auth=basic_auth.authenticate())
@ -131,8 +139,8 @@ 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')
result = [list(x) for x in database.get_song_completions(input_string=input_string)]
return jsonify(result)
else:
return 400
@ -186,9 +194,8 @@ def mark_transferred(entry_id):
@nocache
@basic_auth.required
def set_accept_entries(value):
global accept_entries
if (value == '0' or value == '1'):
accept_entries = bool(int(value))
helpers.set_accept_entries(app,bool(int(value)))
return Response('{"status": "OK"}', mimetype='text/json')
else:
return Response('{"status": "FAIL"}', mimetype='text/json', status=400)
@ -197,7 +204,7 @@ def set_accept_entries(value):
@app.route("/api/entries/accept")
@nocache
def get_accept_entries():
global accept_entries
accept_entries = helpers.get_accept_entries(app)
return Response('{"status": "OK", "value": '+str(int(accept_entries))+'}', mimetype='text/json')
@ -229,6 +236,7 @@ def admin():
@app.before_first_request
def activate_job():
helpers.load_dbconfig(app)
helpers.load_version(app)
helpers.create_data_directory()
database.create_entry_table()
@ -236,6 +244,7 @@ def activate_job():
database.create_done_song_table()
database.create_list_view()
database.create_done_song_view()
database.create_config_table()
helpers.setup_config(app)

View File

@ -1,190 +1,174 @@
# -*- coding: utf_8 -*-
import sqlite3
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 open_db():
conn = sqlite3.connect("/tmp/karaoqueue.db")
conn.execute('PRAGMA encoding = "UTF-8";')
return conn
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=';')
conn = open_db()
cur = conn.cursor()
with get_db_engine().connect() as conn:
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()
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():
conn = open_db()
with get_db_engine().connect() as conn:
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), Transferred INTEGER DEFAULT 0)')
conn.close()
' (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():
conn = open_db()
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)')
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
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
)""")
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, entries.Transferred
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""")
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
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""")
conn.close()
def create_config_table():
with get_db_engine().connect() as conn:
conn.execute("""CREATE TABLE IF NOT EXISTS `config` (
`Key` VARCHAR(50) NOT NULL PRIMARY KEY,
`Value` TEXT
)""")
def get_list():
conn = open_db()
conn.row_factory = sqlite3.Row
cur = conn.cursor()
cur.execute("SELECT * FROM Liste")
with get_db_engine().connect() as conn:
cur = conn.execute("SELECT * FROM Liste")
return cur.fetchall()
def get_played_list():
conn = open_db()
cur = conn.cursor()
cur.execute("SELECT * FROM Abspielliste")
with get_db_engine().connect() as conn:
cur = conn.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;")
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):
conn = open_db()
cur = conn.cursor()
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.execute(
"SELECT Title || \" - \" || Artist AS Song, Id FROM songs WHERE REPLACE(REPLACE(REPLACE(REPLACE(UPPER( SONG ),'ö','Ö'),'ü','Ü'),'ä','Ä'),'ß','') LIKE (?) LIMIT 20;", (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()
cur = conn.cursor()
cur.execute(
"INSERT INTO entries (Song_Id,Name,Client_Id) VALUES(?,?,?);", (song_id, name, client_id))
conn.commit()
conn.close()
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):
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()
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)
conn.close()
return True
def toggle_transferred(entry_id):
conn = open_db()
cur = conn.cursor()
cur.execute("SELECT Transferred FROM entries WHERE ID =?", (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):
cur.execute(
"UPDATE entries SET Transferred = 1 WHERE ID =?", (entry_id,))
conn.execute(
"UPDATE entries SET Transferred = 1 WHERE ID =%s", (entry_id,))
else:
cur.execute(
"UPDATE entries SET Transferred = 0 WHERE ID =?", (entry_id,))
conn.commit()
conn.close()
conn.execute(
"UPDATE entries SET Transferred = 0 WHERE ID =%s", (entry_id,))
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,))
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():
conn = open_db()
cur = conn.cursor()
cur.execute("SELECT Count(*) FROM entries")
with get_db_engine().connect() as conn:
cur = conn.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()
with get_db_engine().connect() as conn:
conn.execute("DELETE FROM done_songs")
return True
def delete_entry(id):
conn = open_db()
cur = conn.cursor()
cur.execute("DELETE FROM entries WHERE id=?", (id,))
conn.commit()
conn.close()
with get_db_engine().connect() as conn:
conn.execute("DELETE FROM entries WHERE id=%s", (id,))
return True
@ -193,20 +177,33 @@ def delete_entries(ids):
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()
with get_db_engine().connect() as conn:
cur = conn.execute("DELETE FROM entries WHERE id=%s", idlist)
return cur.rowcount
except sqlite3.Error as error:
except Exception as error:
return -1
def delete_all_entries():
conn = open_db()
cur = conn.cursor()
cur.execute("DELETE FROM entries")
conn.commit()
conn.close()
def delete_all_entries() -> bool:
with get_db_engine().connect() as conn:
conn.execute("DELETE FROM entries")
return True
def get_config(key: str) -> str:
with get_db_engine().connect() as conn:
cur = conn.execute("SELECT `Value` FROM config WHERE `Key`=%s", (key,))
return cur.fetchall()[0][0]
def set_config(key: str, value: str) -> bool:
with get_db_engine().connect() as conn:
conn.execute("INSERT INTO config (`Key`, `Value`) VALUES (%s,%s) ON DUPLICATE KEY UPDATE `Value`=%s", (key, value, value))
return True
def get_config_list() -> dict:
with get_db_engine().connect() as conn:
cur = conn.execute("SELECT * FROM config")
result_dict = {}
for row in cur.fetchall():
result_dict[row[0]] = row[1]
return result_dict

View File

@ -3,9 +3,10 @@ from bs4 import BeautifulSoup
import json
import os
import uuid
from flask import make_response
from flask import make_response, Flask
from functools import wraps, update_wrapper
from datetime import datetime
import database
data_directory = "data"
config_file = data_directory+"/config.json"
@ -33,11 +34,19 @@ def is_valid_uuid(val):
return False
def check_config_exists():
return os.path.isfile(config_file)
eng = database.get_db_engine()
with eng.connect() as conn:
if conn.dialect.has_table(conn, 'config'):
if (conn.execute("SELECT COUNT(*) FROM config").fetchone()[0] > 0): # type: ignore
return True
else:
return False
else:
return False
def load_version(app):
def load_version(app: Flask):
if os.environ.get("SOURCE_VERSION"):
app.config['VERSION'] = os.environ.get("SOURCE_VERSION")[0:7]
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', '')
@ -48,29 +57,89 @@ def load_version(app):
else:
app.config['VERSION'] = ""
def setup_config(app):
def load_dbconfig(app: Flask):
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:
exit("No database connection string found. Cannot continue. Please set the environment variable DBSTRING or create a file .dbconn in the root directory of the project.")
# Check if config exists in DB, if not, create it.
def setup_config(app: Flask):
if check_config_exists():
config = json.load(open(config_file))
with open(config_file, 'r') as handle:
config = json.load(handle)
config = database.get_config_list()
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")
config = {'username': 'admin', 'password': 'changeme', 'entryquota': 3, 'maxqueue': 20, 'entries_allowed': 1, 'theme': 'default'}
for key, value in config.items():
database.set_config(key, value)
print("Created 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']
app.config['ENTRIES_ALLOWED'] = bool(config['entries_allowed'])
# set queue admittance
def set_accept_entries(app: Flask, allowed: bool):
if allowed:
app.config['ENTRIES_ALLOWED'] = True
database.set_config('entries_allowed', '1')
else:
app.config['ENTRIES_ALLOWED'] = False
database.set_config('entries_allowed', '0')
# get queue admittance
def get_accept_entries(app: Flask) -> bool:
state = bool(int(database.get_config('entries_allowed')))
app.config['ENTRIES_ALLOWED'] = state
return state
# Write settings from current app.config to DB
def persist_config(app: Flask):
config = {'username': app.config['BASIC_AUTH_USERNAME'], 'password': app.config['BASIC_AUTH_PASSWORD'], 'entryquota': app.config['ENTRY_QUOTA'], 'maxqueue': app.config['MAX_QUEUE']}
for key, value in config.items():
database.set_config(key, value)
# Get available themes from themes directory
def get_themes():
themes = []
for theme in os.listdir('./static/css/themes'):
themes.append(theme)
return themes
# Set theme
def set_theme(app: Flask, theme: str):
if theme in get_themes():
app.config['THEME'] = theme
database.set_config('theme', theme)
else:
print("Theme not found, not setting theme.")
def nocache(view):
@wraps(view)
def no_cache(*args, **kwargs):
response = make_response(view(*args, **kwargs))
response.headers['Last-Modified'] = datetime.now()
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'

View File

@ -3,3 +3,5 @@ pandas
Flask-BasicAuth
bs4
gunicorn
SQLAlchemy
mysqlclient

View File

@ -1,8 +1,36 @@
body {
padding-top: 5rem;
:root {
/* Navbar */
--navbar-background-color: #343a40;
--navbar-text-color: rgba(255, 255, 255, .5);
--navbar-text-color-hover: rgba(255, 255, 255, .75);
--navbar-text-color-active: rgba(255, 255, 255, 1);
/* Common */
--background-color: #ffffff;
--background-color-var: #f5f5f5;
--text-color: #212529;
--text-color-var: #343a40;
/* Modals */
--modal-background-color: #ffffff;
--modal-separator-color: #dee2e6;
--modal-close-color: #212529;
/* Tables */
--table-border-color: #dee2e6;
/* Input */
--input-background-color: #ffffff;
}
html, body {
body {
padding-top: 5rem;
background-color: var(--background-color);
}
html,
body {
height: 100%;
}
@ -12,7 +40,8 @@ html, body {
}
main {
padding-bottom: 60px; /* Höhe des Footers */
padding-bottom: 60px;
/* Höhe des Footers */
}
.footer {
@ -21,7 +50,7 @@ main {
height: 60px;
/* Set the fixed height of the footer here */
/*line-height: 60px; /* Vertically center the text there */
background-color: #f5f5f5;
background-color: var(--background-color-var);
}
.topbutton {
@ -56,6 +85,10 @@ table td:first-child {
max-width: 200px !important;
}
.fa-solid {
vertical-align: auto;
}
@media (min-width: 768px) {
.topbutton {
width: auto;
@ -75,3 +108,83 @@ table td:first-child {
display: none;
}
}
body {
background-color: var(--background-color);
color: var(--text-color);
}
.footer {
background-color: #232323;
}
.modal-content {
background-color: var(--background-color);
color: var(--text-color);
}
.modal-header {
background-color: var(--background-color);
color: var(--text-color-var);
border-color: var(var(--modal-separator-color));
}
.modal-footer {
background-color: var(--background-color);
color: var(--text-color-var);
border-color: var(var(--modal-separator-color));
}
.form-control {
background-color: var(--input-background-color);
color: var(--text-color)
}
.form-control:focus {
background-color: var(--input-background-color);
color: var(--text-color)
}
.table td,
.table th {
border-color: var(--table-border-color)
}
.table thead th {
border-color: var(--table-border-color)
}
.close {
color: var(--text-color)
}
pre {
color: var(--text-color-var)
}
@media (prefers-color-scheme: dark) {
:root {
/* Navbar */
--navbar-background-color: #343a40;
--navbar-text-color: rgba(255, 255, 255, .5);
--navbar-text-color-hover: rgba(255, 255, 255, .75);
--navbar-text-color-active: rgba(255, 255, 255, 1);
/* Common */
--background-color: #121212;
--background-color-var: #232323;
--text-color: #f5f5f5;
--text-color-var: #a2a2a2;
/* Modals */
--modal-background-color: #121212;
--modal-separator-color: #232323;
--modal-close-color: #f5f5f5;
/* Tables */
--table-border-color: #232323;
/* Input */
--input-background-color: #343434;
}
}

View File

View File

@ -0,0 +1,91 @@
.navbar {
background: #090a28 !important;
}
.navbar .navbar-toggle:hover,
.navbar .navbar-toggle:focus {
background-color: #900000 !important;
}
.navbar .navbar-toggle {
border: none;
}
.navbar .navbar-nav>.open>a,
.navbar .navbar-nav>.open>a:hover,
.navbar .navbar-nav>.open>a:focus {
color: #CF2323 !important;
background-color: #050515 !important;
}
.btn-primary {
background-color: #15175b;
border-color: #15175b;
}
.btn-primary:hover,
.btn-primary:focus {
background-color: #0e103e;
background-position: 0 -15px;
}
.btn-primary:active,
.btn-primary.active {
background-color: #0e103e;
border-color: #0e103e;
}
.btn-primary.disabled,
.btn-primary:disabled,
.btn-primary[disabled] {
background-color: #0e103e;
background-image: none;
}
.dropdown-menu>li>a:hover,
.dropdown-menu>li>a:focus {
color: #8A0711;
}
.dropdown-menu>.active>a,
.dropdown-menu>.active>a:hover,
.dropdown-menu>.active>a:focus {
background-color: #2e6da4;
background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
background-repeat: repeat-x;
}
.navbar .navbar-nav>li>a:hover {
color: #b60000;
}
.form-control:focus {
border-color: rgba(21, 23, 91, 0.8);
outline: 0;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075),
0 0 8px rgba(21, 23, 91, 0.6);
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset,
0 0 8px rgba(21, 23, 91, 0.6);
}
a {
color: #900000;
}
a:hover,
a:focus {
color: #670000;
}
.navbar-brand {
display: flex;
align-items: center;
}
.navbar-brand > img {
height: 4rem;
}

View File

@ -6,13 +6,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<meta name="color-scheme" content="light dark" />
<link rel="icon" href="favicon.ico">
<link rel="manifest" href="/static/manifest.webmanifest">
<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"
@ -22,12 +23,16 @@
<link href="static/css/style.css" rel="stylesheet">
<!-- Fontawesome Icons -->
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.2/css/all.css"
integrity="sha384-oS3vJWv+0UjzBfQzYUhtDYW+Pj2yciDJxpsK1OYPAYjqT085Qq/1cq5FLXAZQ7Ay" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw=="
crossorigin="anonymous" referrerpolicy="no-referrer" />
<!-- Bootstraptoggle -->
<link href="https://gitcdn.github.io/bootstrap-toggle/2.2.2/css/bootstrap-toggle.min.css" rel="stylesheet">
<!-- Active Theme -->
<link href="static/css/themes/{{config['THEME']}}" rel="stylesheet">
</head>
<body>
@ -73,9 +78,9 @@
{% 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>
{% 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 {{karaoqueue_version}} -&nbsp;<span>&copy</span>&nbsp;2019-21 - Phillip
<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_version}} -&nbsp;2019-23 - Phillip
Kühne</span>
</div>
</footer>
@ -96,9 +101,9 @@
<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.21.2/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>
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>
@ -122,7 +127,7 @@
function loadOrGenerateClientId() {
if (!localStorage.getItem("clientId")) {
localStorage.setItem("clientId",create_UUID())
localStorage.setItem("clientId", create_UUID())
}
}
</script>

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()
})
@ -195,17 +195,19 @@ table td:nth-child(2) {
});
}
function TableActions (value, row, index) {
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.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.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>";
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.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.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>";
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

@ -10,9 +10,20 @@
<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>
<p>
<label for="theme">Aktives Theme</label>
<select class="form-control" id="theme" name="theme">
{% for theme in themes %}
<option value="{{theme}}" {% if theme == config['THEME'] %}selected{% endif %}>{{theme}}</option>
{% endfor %}
</select>
</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>
<details>
<summary>Current config:</summary>
<pre>{% for key, val in config.items() %}{{key}}: {{val}}<br>{% endfor %}</pre>
</details>
{% endblock %}
{% block extrajs %}
{% endblock %}

View File

@ -46,7 +46,7 @@
var items = [];
$.each(data, function (key, val) {
items.push("<tr><td>" + val[0] + `</td>
<td><button type='button'
<td class='buttoncell'><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

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

@ -0,0 +1,18 @@
version: "3.9"
secrets:
secrets:
file: ./secrets.yml
services:
karaoqueue:
image: "phillipkhne/karaoqueue:latest"
build: .
restart: always
ports:
- "127.0.0.1:8081:80" # Please put a reverse proxy in front of this
env_file: .env
db:
image: mariadb
restart: always
env_file: .env

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"