Compare commits

...

13 Commits

Author SHA1 Message Date
06594855d6 Fix stuck tooltips
Fix stuck tooltips by implementing custom data  request function, and destroying existing tooltips on new data load.

Fixes #81
2024-02-16 12:32:37 +01:00
43a12228ae Update used libraries 2024-02-16 12:30:28 +01:00
eb0cfcf4cb Merge pull request #80 from PhoenixTwoFive/security-fixes
Unpin dependencies in Pipfile to keep updated to latest version.
2024-02-13 18:39:40 +01:00
6f1628546e Update requirements.txt accordingly. 2024-02-13 18:31:08 +01:00
28d965ee41 Unpin dependencies in Pipfile to keep updated to latest version. 2024-02-13 18:14:03 +01:00
68c5771fc6 Clean up old config 2024-02-13 17:48:17 +01:00
538f0a3c23 Do not generate EventID during create_schema() 2023-12-13 18:53:33 +00:00
601a062eac Heh. 2023-10-06 16:41:08 +00:00
9fadef965a Merge pull request #79 from PhoenixTwoFive/77-past-playback-based-score-and-recommendations
77 past playback based score and recommendations
2023-10-06 18:29:06 +02:00
d1b865f16f Remove unnecessary debug logs 2023-10-06 16:19:07 +00:00
d03b46100c Remove Information exposure. 2023-10-06 15:59:21 +00:00
6d2941cfca Statistik Import- und Export. 2023-10-06 15:46:50 +00:00
a1da421ffe Add suggestions 2023-10-04 18:37:08 +00:00
12 changed files with 404 additions and 155 deletions

View File

@ -1,5 +1,4 @@
{ {
"python.pythonPath": "/usr/bin/python",
"python.testing.unittestArgs": [ "python.testing.unittestArgs": [
"-v", "-v",
"-s", "-s",
@ -9,8 +8,6 @@
], ],
"python.testing.pytestEnabled": false, "python.testing.pytestEnabled": false,
"python.testing.unittestEnabled": true, "python.testing.unittestEnabled": true,
"python.linting.pylintEnabled": false,
"python.linting.flake8Enabled": true,
"emmet.includeLanguages": { "emmet.includeLanguages": {
"django-html": "html" "django-html": "html"
} }

View File

@ -4,45 +4,45 @@ verify_ssl = true
name = "pypi" name = "pypi"
[packages] [packages]
autopep8 = "==2.0.2" autopep8 = "*"
beautifulsoup4 = "==4.12.0" beautifulsoup4 = "*"
bs4 = "==0.0.1" bs4 = "*"
certifi = "==2022.12.7" certifi = "*"
charset-normalizer = "==3.1.0" charset-normalizer = "*"
click = "==8.1.3" click = "*"
flake8 = "==6.0.0" flake8 = "*"
flask = "==2.2.3" flask = "*"
flask-basicauth = "==0.2.0" flask-basicauth = "*"
greenlet = "==2.0.2" greenlet = "*"
gunicorn = "==20.1.0" gunicorn = "*"
idna = "==3.4" idna = "*"
itsdangerous = "==2.1.2" itsdangerous = "*"
jinja2 = "==3.1.2" jinja2 = "*"
mariadb = "==1.1.6" mariadb = "*"
markupsafe = "==2.1.2" markupsafe = "*"
mccabe = "==0.7.0" mccabe = "*"
mysql = "==0.0.3" mysql = "*"
mysqlclient = "==2.1.1" mysqlclient = "*"
numpy = "==1.24.2" numpy = "*"
packaging = "==23.0" packaging = "*"
pandas = "==1.5.3" pandas = "*"
pycodestyle = "==2.10.0" pycodestyle = "*"
pyflakes = "==3.0.1" pyflakes = "*"
pymysql = "==1.0.3" pymysql = "*"
python-dateutil = "==2.8.2" python-dateutil = "*"
pytz = "==2023.3" pytz = "*"
requests = "==2.28.2" requests = "*"
six = "==1.16.0" six = "*"
soupsieve = "==2.4" soupsieve = "*"
sqlalchemy = "==2.0.7" sqlalchemy = "*"
toml = "==0.10.2" toml = "*"
tomli = "==2.0.1" tomli = "*"
typing-extensions = "==4.5.0" typing-extensions = "*"
urllib3 = "==1.26.15" urllib3 = "*"
werkzeug = "==2.2.3" werkzeug = "*"
[dev-packages] [dev-packages]
[requires] [requires]
python_version = "3.9" python_version = "3.11"
python_full_version = "3.9.17" python_full_version = "3.11.7"

View File

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

View File

@ -7,6 +7,7 @@ import os
import json import json
from flask_basicauth import BasicAuth from flask_basicauth import BasicAuth
from helpers import nocache from helpers import nocache
from werkzeug.utils import secure_filename
app = Flask(__name__, static_url_path='/static') app = Flask(__name__, static_url_path='/static')
basic_auth = BasicAuth(app) basic_auth = BasicAuth(app)
@ -170,6 +171,70 @@ def query_songs_with_details(input_string=""):
return jsonify(result) return jsonify(result)
@app.route("/api/songs/suggest")
@nocache
def query_songs_with_details_suggest(input_string=""):
input_string = request.args.get("count", input_string)
if input_string == "":
return Response(status=400)
result = []
if not input_string.isnumeric():
return Response(status=400)
count: int = int(input_string)
for x in database.get_song_suggestions(count):
# Turn row into dict. Add field labels.
result.append(dict(zip(['karafun_id', 'title', 'artist', 'year', 'duo', 'explicit', 'styles', 'languages'], x)))
return jsonify(result)
@app.route("/api/songs/stats")
@nocache
# Return the data from long_term_stats as json
def get_stats():
db_result = database.get_long_term_stats()
data = []
for row in db_result:
data.append(dict(zip(['id', 'count'], row)))
return jsonify(data)
@app.route("/api/songs/stats.csv")
@nocache
# Return data from long_term_stats as csv
def get_stats_csv():
db_result = database.get_long_term_stats()
print(db_result)
csv = "Id,Playbacks\n"
for row in db_result:
csv += str(row[0]) + "," + str(row[1]) + "\n"
return Response(csv, mimetype='text/csv')
@app.route("/api/songs/stats.csv", methods=['POST'])
@nocache
@basic_auth.required
# Update long_term_stats from csv
def update_stats_csv():
if not request.files:
abort(400)
file = request.files['file']
if file.filename is None:
abort(400)
else:
filename = secure_filename(file.filename)
if filename == '':
abort(400)
if not filename.endswith('.csv'):
abort(400)
if file:
if database.import_stats(file):
return Response('{"status": "OK"}', mimetype='text/json')
else:
return Response('{"status": "FAIL"}', mimetype='text/json', status=400)
else:
abort(400)
@app.route("/api/songs/details/<song_id>") @app.route("/api/songs/details/<song_id>")
def get_song_details(song_id): def get_song_details(song_id):
result = database.get_song_details(song_id) result = database.get_song_details(song_id)
@ -261,14 +326,20 @@ def get_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/event/close")
@nocache @nocache
@basic_auth.required @basic_auth.required
def clear_played_songs(): def close_event():
if database.clear_played_songs(): try:
database.transfer_playbacks()
database.clear_played_songs()
database.delete_all_entries()
helpers.reset_current_event_id(app)
return Response('{"status": "OK"}', mimetype='text/json') return Response('{"status": "OK"}', mimetype='text/json')
else: except Exception:
return Response('{"status": "FAIL"}', mimetype='text/json') response = jsonify({"status": "FAIL", "message": "An error occured while closing the event."})
response.status_code = 400
return response
@app.route("/api/entries/delete_all") @app.route("/api/entries/delete_all")
@ -298,6 +369,7 @@ def activate_job():
with app.app_context(): with app.app_context():
helpers.load_dbconfig(app) helpers.load_dbconfig(app)
helpers.load_version(app) helpers.load_version(app)
database.create_schema()
database.create_entry_table() database.create_entry_table()
database.create_song_table() database.create_song_table()
database.create_done_song_table() database.create_done_song_table()

View File

@ -40,6 +40,30 @@ def import_songs(song_csv):
return ("Imported songs ({} in Database)".format(num_songs)) return ("Imported songs ({} in Database)".format(num_songs))
def import_stats(stats_csv):
print("Start importing Stats...")
df = pandas.read_csv(stats_csv, sep=',')
if (df.columns[0] != "Id" or df.columns[1] != "Playbacks"):
return False
with get_db_engine().connect() as conn:
for index, row in df.iterrows():
stmt = text(
"INSERT INTO long_term_stats (Id,Playbacks) VALUES (:par_id,:par_playbacks) ON DUPLICATE KEY UPDATE Playbacks=:par_playbacks")
conn.execute(stmt, {"par_id": row["Id"], "par_playbacks": row["Playbacks"]})
conn.commit()
return True
def create_schema():
create_song_table()
create_entry_table()
create_done_song_table()
create_config_table()
create_long_term_stats_table()
create_list_view()
create_done_song_view()
def create_entry_table(): def create_entry_table():
with get_db_engine().connect() as conn: with get_db_engine().connect() as conn:
stmt = text( stmt = text(
@ -75,6 +99,17 @@ def create_song_table():
conn.commit() conn.commit()
def create_long_term_stats_table():
with get_db_engine().connect() as conn:
stmt = text("""CREATE TABLE IF NOT EXISTS `long_term_stats` (
`Id` INTEGER,
`Playbacks` INTEGER,
PRIMARY KEY (`Id`)
)""")
conn.execute(stmt)
conn.commit()
def create_list_view(): def create_list_view():
with get_db_engine().connect() as conn: with get_db_engine().connect() as conn:
stmt = text("""CREATE OR REPLACE VIEW `Liste` AS stmt = text("""CREATE OR REPLACE VIEW `Liste` AS
@ -121,6 +156,34 @@ def get_played_list():
return cur.fetchall() return cur.fetchall()
def get_song_suggestions(count: int):
with get_db_engine().connect() as conn:
# Get the top 10 songs with the most plays from the long_term_stats table and join them with the songs table to get the song details.
# Exclude songs that are already in the queue, or in the done_songs table.
stmt = text("""
SELECT s.Id, s.Title, s.Artist, s.Year, s.Duo, s.Explicit, s.Styles, s.Languages
FROM long_term_stats lts
LEFT JOIN songs s ON lts.Id = s.Id
LEFT JOIN entries e ON lts.Id = e.Song_Id
LEFT JOIN done_songs ds ON lts.Id = ds.Song_Id
WHERE e.Id IS NULL AND ds.Song_Id IS NULL
ORDER BY lts.Playbacks DESC
LIMIT :count;
""")
cur = conn.execute(stmt, {"count": count})
return cur.fetchall()
def get_long_term_stats():
with get_db_engine().connect() as conn:
stmt = text("""
SELECT lts.Id, lts.Playbacks
FROM long_term_stats lts
""")
cur = conn.execute(stmt)
return cur.fetchall()
def get_song_list(): def get_song_list():
with get_db_engine().connect() as conn: with get_db_engine().connect() as conn:
stmt = text("SELECT Artist || \" - \" || Title AS Song, Id FROM songs;") stmt = text("SELECT Artist || \" - \" || Title AS Song, Id FROM songs;")
@ -224,6 +287,23 @@ def check_queue_length():
return cur.fetchall()[0][0] return cur.fetchall()[0][0]
def transfer_playbacks():
with get_db_engine().connect() as conn:
# Use SQL to update the long_term_stats table. Add the playbacks of the songs in the done_songs table to the playbacks of the songs in the long_term_stats table.
stmt = text("""
INSERT INTO long_term_stats(Id, Playbacks)
SELECT ds.Song_Id, ds.Plays
FROM done_songs ds
LEFT JOIN long_term_stats lts ON ds.Song_Id = lts.Id
ON DUPLICATE KEY
UPDATE Playbacks = lts.Playbacks + VALUES(Playbacks);
""")
result = conn.execute(stmt)
print(result)
conn.commit()
return True
def clear_played_songs(): def clear_played_songs():
with get_db_engine().connect() as conn: with get_db_engine().connect() as conn:
conn.execute(text("DELETE FROM done_songs")) conn.execute(text("DELETE FROM done_songs"))

View File

@ -1,36 +1,40 @@
autopep8~=2.0.2 autopep8==2.0.4
beautifulsoup4~=4.12.0 beautifulsoup4==4.12.3
bs4~=0.0.1 blinker==1.7.0
certifi~=2022.12.7 bs4==0.0.2
charset-normalizer~=3.1.0 certifi==2024.2.2
click~=8.1.3 charset-normalizer==3.3.2
flake8~=6.0.0 click==8.1.7
Flask~=2.3.2 flake8==7.0.0
Flask-BasicAuth~=0.2.0 Flask==3.0.2
greenlet~=2.0.2 Flask-BasicAuth==0.2.0
gunicorn~=20.1.0 greenlet==3.0.3
idna~=3.4 gunicorn==21.2.0
itsdangerous~=2.1.2 idna==3.6
Jinja2~=3.1.2 itsdangerous==2.1.2
mariadb~=1.1.6 Jinja2==3.1.3
MarkupSafe~=2.1.2 mariadb==1.1.10
mccabe~=0.7.0 MarkupSafe==2.1.5
mysql~=0.0.3 mccabe==0.7.0
mysqlclient~=2.1.1 mysql==0.0.3
numpy~=1.24.2 mysqlclient==2.2.4
packaging~=23.0 numpy==1.26.4
pandas~=1.5.3 packaging==23.2
pycodestyle~=2.10.0 pandas==2.2.0
pyflakes~=3.0.1 pipfile-upgrade==0.0.2
PyMySQL~=1.0.3 pycodestyle==2.11.1
python-dateutil~=2.8.2 pyflakes==3.2.0
pytz~=2023.3 PyMySQL==1.1.0
requests~=2.31.0 python-dateutil==2.8.2
six~=1.16.0 pytz==2024.1
soupsieve~=2.4 requests==2.31.0
SQLAlchemy~=2.0.7 six==1.16.0
toml~=0.10.2 soupsieve==2.5
tomli~=2.0.1 SQLAlchemy==2.0.27
typing_extensions~=4.5.0 toml==0.10.2
urllib3~=1.26.15 tomli==2.0.1
Werkzeug~=3.0.0 tomlkit==0.12.3
typing_extensions==4.9.0
tzdata==2024.1
urllib3==2.2.0
Werkzeug==3.0.1

View File

@ -13,7 +13,7 @@
<title>{% block title %}{% endblock %} - KaraoQueue</title> <title>{% block title %}{% endblock %} - KaraoQueue</title>
<!-- Bootstrap-Tables --> <!-- Bootstrap-Tables -->
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.21.2/dist/bootstrap-table.min.css"> <link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.22.2/dist/bootstrap-table.min.css">
<!-- Bootstrap-Toaster--> <!-- Bootstrap-Toaster-->
<link rel="stylesheet" <link rel="stylesheet"
@ -22,8 +22,8 @@
crossorigin="anonymous" referrerpolicy="no-referrer" /> crossorigin="anonymous" referrerpolicy="no-referrer" />
<!-- Bootstrap core CSS --> <!-- Bootstrap core CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.2.1/dist/css/bootstrap.min.css" <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css"
integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" crossorigin="anonymous"> integrity="sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N" crossorigin="anonymous">
<!-- Custom styles for this template --> <!-- Custom styles for this template -->
<link href="static/css/style.css" rel="stylesheet"> <link href="static/css/style.css" rel="stylesheet">
@ -86,8 +86,8 @@
{% endif %} {% endif %}
<a href="https://github.com/PhoenixTwoFive/karaoqueue" class="ml-1 mr-1"><i <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="fab fa-github mr-1"></i><span>Github</span></a>
<span class="text-muted"> {{karaoqueue_version}} -&nbsp;2019-23 - Phillip <span class="text-muted"> {{karaoqueue_version}} -&nbsp;2019-23 - <span id="myName">Phillip
Kühne</span> Kühne</span></span>
</div> </div>
</footer> </footer>
<!-- Footer --> <!-- Footer -->
@ -101,15 +101,15 @@
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.14.6/dist/umd/popper.min.js" <script src="https://cdn.jsdelivr.net/npm/popper.js@1.14.6/dist/umd/popper.min.js"
integrity="sha384-wHAiFfRlMFy6i5SRaxvfOCifBUQy1xHdJ/yoi7FRNXMRBu5WHdZYu1hA6ZOblgut" integrity="sha384-wHAiFfRlMFy6i5SRaxvfOCifBUQy1xHdJ/yoi7FRNXMRBu5WHdZYu1hA6ZOblgut"
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.2.1/dist/js/bootstrap.min.js" <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.min.js"
integrity="sha384-B0UglyR+jN6CkvvICOB2joaf5I4l3gm9GU6Hc1og6Ls7i6U/mkkaduKaBhlAXv9k" integrity="sha384-+sLIOodYLS7CIrQpBjl+C7nPvqq+FbNUBDunl/OZv93DB7Ln/533i8e/mZXLi/P+"
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/4.4.0/bootbox.min.js" <script src="https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/4.4.0/bootbox.min.js"
integrity="sha256-4F7e4JsAJyLUdpP7Q8Sah866jCOhv72zU5E8lIRER4w=" crossorigin="anonymous"> integrity="sha256-4F7e4JsAJyLUdpP7Q8Sah866jCOhv72zU5E8lIRER4w=" crossorigin="anonymous">
</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.22.2/dist/bootstrap-table.min.js"></script>
<script <script
src="https://unpkg.com/bootstrap-table@1.21.2/dist/extensions/auto-refresh/bootstrap-table-auto-refresh.min.js"></script> src="https://unpkg.com/bootstrap-table@1.22.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> <script src="https://gitcdn.github.io/bootstrap-toggle/2.2.2/js/bootstrap-toggle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-toaster/4.1.2/js/bootstrap-toaster.min.js" <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-toaster/4.1.2/js/bootstrap-toaster.min.js"
@ -123,6 +123,9 @@
var pathname = window.location.pathname; var pathname = window.location.pathname;
$('.navbar-nav > li > a[href="' + pathname + '"]').parent().addClass('active'); $('.navbar-nav > li > a[href="' + pathname + '"]').parent().addClass('active');
$('[data-toggle="tooltip"]').tooltip() $('[data-toggle="tooltip"]').tooltip()
setTimeout(function () {
$('#myName').text("Klack-Klack")
}, Math.floor(Math.random() * 20000) + 20000);
}) })
function create_UUID() { function create_UUID() {
@ -184,6 +187,14 @@
return entries; return entries;
} }
function dataRequestQueueAndDestroyTooltips(params) {
document.querySelectorAll(".tooltip[role='tooltip']").forEach((tooltip) => tooltip.remove());
var base_url = "/api/queue"
$.get(base_url).then((res) => {
params.success(res);
});
}
</script> </script>
</body> </body>

View File

@ -4,7 +4,7 @@
<a id="bfb" role="button" class="btn btn-primary btn-lg btn-block mb-2" href="/list">Eintragen</a> <a id="bfb" role="button" class="btn btn-primary btn-lg btn-block mb-2" href="/list">Eintragen</a>
<table class="table entries" <table class="table entries"
data-toggle="table" data-toggle="table"
data-url="/api/queue" data-ajax="dataRequestQueueAndDestroyTooltips"
data-pagination="true" data-pagination="true"
data-classes="table" data-classes="table"
data-show-refresh="false" data-show-refresh="false"
@ -35,8 +35,6 @@
}) })
function TableActionsFormatter(value, row, index) { function TableActionsFormatter(value, row, index) {
console.log("Value: " + value + ", Row: " + row + ", Index: " + index)
console.log(row)
if (getOwnedEntries().includes(row.entry_ID)) { if (getOwnedEntries().includes(row.entry_ID)) {
return "<button type='button' class='btn btn-danger' data-toggle='tooltip' data-placement='top' title='Eintrag zurückziehen' onclick=\"event.stopPropagation();$(this).tooltip('dispose');requestDeletionAsUser(" + row["entry_ID"] + ")\"><i class='fas fa-trash'></i></button>" return "<button type='button' class='btn btn-danger' data-toggle='tooltip' data-placement='top' title='Eintrag zurückziehen' onclick=\"event.stopPropagation();$(this).tooltip('dispose');requestDeletionAsUser(" + row["entry_ID"] + ")\"><i class='fas fa-trash'></i></button>"
} }

View File

@ -22,7 +22,7 @@
<table class="table entries" id="entrytable" data-toggle="table" data-search="true" data-show-columns="true" <table class="table entries" id="entrytable" data-toggle="table" data-search="true" data-show-columns="true"
data-show-toggle="true" data-multiple-select-row="true" data-click-to-select="true" data-toolbar="#toolbar" data-show-toggle="true" data-multiple-select-row="true" data-click-to-select="true" data-toolbar="#toolbar"
data-pagination="true" data-show-extended-pagination="true" data-classes="table table-hover" data-pagination="true" data-show-extended-pagination="true" data-classes="table table-hover"
data-url="/api/queue" data-show-refresh="true" data-auto-refresh="true" data-auto-refresh-interval="10"> data-ajax="dataRequestQueueAndDestroyTooltips" data-show-refresh="true" data-auto-refresh="true" data-auto-refresh-interval="10">
<thead> <thead>
<tr> <tr>
<th data-field="state" data-checkbox="true"></th> <th data-field="state" data-checkbox="true"></th>
@ -153,7 +153,7 @@
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: '/api/entries/delete', url: '/api/entries/delete',
data: JSON.stringify(ids), // or JSON.stringify ({name: 'jonas'}), data: JSON.stringify(ids),
error: function () { error: function () {
bootbox.alert({ bootbox.alert({
message: "Fehler beim Löschen der Eintragungen.", message: "Fehler beim Löschen der Eintragungen.",

View File

@ -2,8 +2,8 @@
{% block title %}Abspielliste{% endblock %} {% block title %}Abspielliste{% endblock %}
{% block content %} {% block content %}
<div id="toolbar"> <div id="toolbar">
<button type="button" class="topbutton btn btn-danger" onclick="confirmDeleteAllEntries()"><i <button type="button" class="topbutton btn btn-danger" onclick="confirmCloseEvent()"><i
class="fas fa-trash mr-2"></i>Abspielliste löschen</button> class="fas fa-trash mr-2"></i>Event beenden</button>
<button type="button" class="topbutton btn btn-primary" onclick="exportPDF()"><i <button type="button" class="topbutton btn btn-primary" onclick="exportPDF()"><i
class="fas fa-file-pdf mr-2"></i>Als PDF herunterladen</button> class="fas fa-file-pdf mr-2"></i>Als PDF herunterladen</button>
<button type="button" class="topbutton btn btn-secondary" onclick="printPDF()"><i <button type="button" class="topbutton btn btn-secondary" onclick="printPDF()"><i
@ -41,9 +41,17 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.5.3/jspdf.min.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 src="https://unpkg.com/jspdf-autotable@3.0.10/dist/jspdf.plugin.autotable.js"></script>
<script> <script>
function confirmDeleteAllEntries() { function confirmCloseEvent() {
bootbox.confirm({ bootbox.confirm({
message: "Wirklich Abspielliste löschen?<br>Stelle sicher, dass du sie vorher zwecks Abrechnung gedruckt und/oder heruntergeladen hast!", message: `Wirklich Ereignis beenden?<br>
Folgendes wird passieren:<br>
<ul>
<li>Die Warteschlange wird geleert</li>
<li>Die Abspielliste wird gelöscht</li>
<li>Eine neue Event-ID wird vergeben</li>
</ul>
Diese Aktion kann nicht rückgängig gemacht werden!
`,
buttons: { buttons: {
confirm: { confirm: {
label: 'Ja', label: 'Ja',
@ -56,15 +64,15 @@
}, },
callback: function(result){ callback: function(result){
if (result) { if (result) {
deleteAllEntries() closeEvent()
} }
} }
}) })
} }
function deleteAllEntries() { function closeEvent() {
$.ajax({ $.ajax({
type: 'GET', type: 'GET',
url: '/api/played/clear', url: '/api/event/close',
contentType: "application/json", contentType: "application/json",
dataType: 'json', dataType: 'json',
async: false async: false

View File

@ -4,7 +4,8 @@
<form method="post"> <form method="post">
<p> <p>
<label for="entryquota">Maximale Anzahl an Einträgen pro Nutzer</label> <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']}}> <input type="number" class="form-control" id="entryquota" name="entryquota" min=1
value={{app.config['ENTRY_QUOTA']}}>
</p> </p>
<p> <p>
<label for="maxqueue">Maximale Anzahl an Einträgen Insgesamt</label> <label for="maxqueue">Maximale Anzahl an Einträgen Insgesamt</label>
@ -24,7 +25,8 @@
</div> </div>
<p> <p>
<label for="username">Benutzername</label> <label for="username">Benutzername</label>
<input type="text" class="form-control" id="username" name="username" value={{app.config['BASIC_AUTH_USERNAME']}}> <input type="text" class="form-control" id="username" name="username"
value={{app.config['BASIC_AUTH_USERNAME']}}>
</p> </p>
<p> <p>
<label for="password">Passwort ändern</label> <label for="password">Passwort ändern</label>
@ -32,10 +34,69 @@
</p> </p>
<input type="submit" class="btn btn-primary mr-1 mb-2" value="Einstellungen anwenden"> <input type="submit" class="btn btn-primary mr-1 mb-2" value="Einstellungen anwenden">
</form> </form>
<form>
<p>
<label for="statsImport">Statistiken importieren/exportieren</label>
</p>
<div class="row">
<div class="col-sm-4">
<a class="btn btn-secondary" type="button" id="statsExport" href="/api/songs/stats.csv"><i class="fas fa-download mr-1"></i>Exportieren</a>
</div>
<div class="col input-group mb-3">
<div class="custom-file mr-1">
<input type="file" class="custom-file-input" id="statsImport" data-allowed-file-extensions='["csv"]'>
<label class="custom-file-label" for="statsImport">CSV-Datei auswählen</label>
</div>
<button class="btn btn-secondary" type="button" id="statsImportBtn"><i class="fas fa-upload mr-1"></i>Importieren</button>
</div>
</div>
<p>
</p>
</form>
<details> <details>
<summary>Current config:</summary> <summary>Current config:</summary>
<pre>{% for key, val in config.items() %}{{key}}: {{val}}<br>{% endfor %}</pre> <pre>{% for key, val in config.items() %}{{key}}: {{val}}<br>{% endfor %}</pre>
</details> </details>
{% endblock %} {% endblock %}
{% block extrajs %} {% block extrajs %}
<script>
$(document).ready(function () {
$('#statsImport').on('change', function () {
var fileName = $(this).val().split('\\').pop();
$(this).next('.custom-file-label').html(fileName);
});
$('#statsImportBtn').on('click', function () {
var file_data = $('#statsImport').prop('files')[0];
var form_data = new FormData();
form_data.append('file', file_data);
$.ajax({
url: '/api/songs/stats.csv',
cache: false,
contentType: false,
processData: false,
data: form_data,
type: 'post',
success: function (response) {
toast = {
title: "Erfolgreich importiert",
message: "Die Statistiken wurden erfolgreich importiert.",
status: TOAST_STATUS.SUCCESS,
timeout: 5000
}
Toast.create(toast);
},
error: function (response) {
toast = {
title: "Fehler beim Importieren",
message: "Die Statistiken konnten nicht importiert werden.",
status: TOAST_STATUS.ERROR,
timeout: 5000
}
Toast.create(toast);
}
});
});
});
</script>
{% endblock %} {% endblock %}

View File

@ -1,7 +1,8 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}Songsuche{% endblock %} {% block title %}Songsuche{% endblock %}
{% block content %} {% block content %}
<input class="form-control" id="filter" type="text" placeholder="Suchen..."> <input class="form-control" id="filter" type="text" placeholder="Suche nach einem Song...">
<h4 id="suggestionExplainer" class="mt-3 mb-3 text-center" style="display: none;">Oder probiere es mit einem dieser Vorschläge:</h4>
<table class="table"> <table class="table">
<tbody id="songtable"> <tbody id="songtable">
</tbody> </tbody>
@ -68,7 +69,7 @@
{% block extrajs %} {% block extrajs %}
<script> <script>
$(document).ready(function () { $(document).ready(function () {
$("#filter").focus(); getSuggestions(10);
$("#filter").keyup(debounce(() => songSearch())); $("#filter").keyup(debounce(() => songSearch()));
$("#nameForm").submit(function (e) { $("#nameForm").submit(function (e) {
@ -78,8 +79,69 @@
$('#enqueueModal').on('shown.bs.modal', function (e) { $('#enqueueModal').on('shown.bs.modal', function (e) {
$("#singerNameInput").focus(); $("#singerNameInput").focus();
}) })
}); });
function getSuggestions(count) {
$.getJSON("/api/songs/suggest", { count: count }, function (data) {
console.log(data);
if (data.length == 0) {
console.log("No suggestions");
$("#suggestionExplainer").hide();
} else {
$("#suggestionExplainer").show();
}
$.each(data, function (key, val) {
$("#songtable").append(constructResultRow(val))
});
});
}
function constructResultRow(data) {
let itemRow = document.createElement("tr")
let itemCell = document.createElement("td")
itemCell.innerHTML = data["artist"] + ` - ` + data["title"]
itemRow.appendChild(itemCell)
let infoCell = document.createElement("td")
let duoindicator = document.createElement("i")
duoindicator.classList.add("fas")
if (data["duo"] == 0) {
duoindicator.classList.add("fa-user")
}
if (data["duo"] == 1) {
duoindicator.classList.add("fa-user-friends")
}
duoindicator.classList.add("ml-1")
duoindicator.classList.add("list-indicator")
infoCell.appendChild(duoindicator)
if (data["explicit"] == 1) {
let explicitindicator = document.createElement("i")
explicitindicator.classList.add("fas")
explicitindicator.classList.add("fa-e")
explicitindicator.classList.add("ml-1")
infoCell.appendChild(explicitindicator)
}
itemRow.appendChild(infoCell)
let buttonCell = document.createElement("td")
let button = document.createElement("button")
button.classList.add("btn")
button.classList.add("btn-primary")
button.classList.add("justify-content-center")
button.classList.add("align-content-between")
button.classList.add("enqueueButton")
button.setAttribute("type", "button")
button.setAttribute("data-toggle", "modal")
button.setAttribute("data-target", "#enqueueModal")
button.setAttribute("onclick", "setSelectedId(" + data["karafun_id"] + ")")
let buttonIcon = document.createElement("i")
buttonIcon.classList.add("fas")
buttonIcon.classList.add("fa-plus")
button.appendChild(buttonIcon)
buttonCell.appendChild(button)
itemRow.appendChild(buttonCell)
return itemRow
}
function enqueue(client_id, id, name, success_callback, blocked_callback) { function enqueue(client_id, id, name, success_callback, blocked_callback) {
var data = { var data = {
@ -108,7 +170,6 @@
$("#selectedId").attr("value", id); $("#selectedId").attr("value", id);
$.getJSON("/api/songs/details/" + id, function (data) { $.getJSON("/api/songs/details/" + id, function (data) {
console.log(data);
$("#songTitle").html(data["title"]); $("#songTitle").html(data["title"]);
$("#songArtist").html(data["artist"]); $("#songArtist").html(data["artist"]);
$("#songYear").html(data["year"]); $("#songYear").html(data["year"]);
@ -191,7 +252,6 @@
var name = $("#singerNameInput").val(); var name = $("#singerNameInput").val();
var id = $("#selectedId").attr("value"); var id = $("#selectedId").attr("value");
enqueue(localStorage.getItem("clientId"), id, name, function (response) { enqueue(localStorage.getItem("clientId"), id, name, function (response) {
console.log(response);
entryID = response["entry_id"]; entryID = response["entry_id"];
toast = { toast = {
title: "Erfolgreich eingetragen", title: "Erfolgreich eingetragen",
@ -200,7 +260,6 @@
timeout: 5000 timeout: 5000
} }
Toast.create(toast); Toast.create(toast);
console.log("Entry ID: " + entryID);
addEntry(entryID); addEntry(entryID);
$("#enqueueModal").modal('hide'); $("#enqueueModal").modal('hide');
window.location.href = '/#end'; window.location.href = '/#end';
@ -219,53 +278,12 @@
function songSearch() { function songSearch() {
let value = $("#filter").val() let value = $("#filter").val()
if (value.length >= 1) { if (value.length >= 1) {
$("#suggestionExplainer").hide();
$.getJSON("/api/songs/search", { q: value }, function (data) { $.getJSON("/api/songs/search", { q: value }, function (data) {
var items = []; var items = [];
$("#songtable").html("") $("#songtable").html("")
$.each(data, function (key, val) { $.each(data, function (key, val) {
let itemRow = document.createElement("tr") $("#songtable").append(constructResultRow(val))
let itemCell = document.createElement("td")
itemCell.innerHTML = val["artist"] + ` - ` + val["title"]
itemRow.appendChild(itemCell)
let infoCell = document.createElement("td")
let duoindicator = document.createElement("i")
duoindicator.classList.add("fas")
if (val["duo"] == 0) {
duoindicator.classList.add("fa-user")
}
if (val["duo"] == 1) {
duoindicator.classList.add("fa-user-friends")
}
duoindicator.classList.add("ml-1")
duoindicator.classList.add("list-indicator")
infoCell.appendChild(duoindicator)
if (val["explicit"] == 1) {
let explicitindicator = document.createElement("i")
explicitindicator.classList.add("fas")
explicitindicator.classList.add("fa-e")
explicitindicator.classList.add("ml-1")
infoCell.appendChild(explicitindicator)
}
itemRow.appendChild(infoCell)
let buttonCell = document.createElement("td")
let button = document.createElement("button")
button.classList.add("btn")
button.classList.add("btn-primary")
button.classList.add("justify-content-center")
button.classList.add("align-content-between")
button.classList.add("enqueueButton")
button.setAttribute("type", "button")
button.setAttribute("data-toggle", "modal")
button.setAttribute("data-target", "#enqueueModal")
button.setAttribute("onclick", "setSelectedId(" + val["karafun_id"] + ")")
let buttonIcon = document.createElement("i")
buttonIcon.classList.add("fas")
buttonIcon.classList.add("fa-plus")
button.appendChild(buttonIcon)
buttonCell.appendChild(button)
itemRow.appendChild(buttonCell)
$("#songtable").append(itemRow)
}); });
@ -274,6 +292,7 @@
}); });
} else { } else {
$("#songtable").html("") $("#songtable").html("")
getSuggestions(10);
} }
} }