Compare commits

...

72 Commits

Author SHA1 Message Date
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
d8899267c3 Merge pull request #76 from PhoenixTwoFive/PhoenixTwoFive-patch-1
Update devcontainer.json
2023-10-04 15:19:02 +02:00
dd83d6c9c4 Update devcontainer.json 2023-10-04 15:18:54 +02:00
14630b97be Move mariabd install to postCreate to fix github codespaces 2023-10-04 13:11:43 +00:00
7f4625a062 Fix CSS quirks 2023-10-04 12:49:06 +00:00
e7d9816010 Add song year to detail view 2023-10-04 12:48:50 +00:00
37990e596c Add python extensions to devcontainer.json 2023-10-04 12:48:28 +00:00
00e090ec48 Remove faulty postAttachCommand from devcontainer.json 2023-10-04 10:45:36 +00:00
e7b0f5f2dc make requirements.txt useful 2023-10-04 01:33:35 +00:00
c32ed395d8 Improve construction tape gimmick 2023-10-04 01:23:21 +00:00
95be876a19 Change terminology 2023-10-04 01:06:16 +00:00
0f9ad4f91a Fix trailing whitespace 2023-10-04 01:02:30 +00:00
ac940ded8c Add debouncing for the search 2023-10-04 00:53:03 +00:00
c8b65e4433 Ensure Index exists after Song import 2023-10-04 00:33:15 +00:00
c50f00c1d3 Fix env var version 2023-10-04 00:24:26 +00:00
3b4152f89f Fix build script 2023-10-04 00:19:14 +00:00
4561f5f376 Merge branch 'legacy' of github.com:PhoenixTwoFive/karaoqueue into legacy 2023-10-04 00:18:33 +00:00
3728e282e3 Fix container build script 2023-10-04 00:18:29 +00:00
6d3ca87869 Merge pull request #75 from PhoenixTwoFive/59-dark-theme-toast-notifications
Dark theme Toast notifications
2023-10-04 02:15:48 +02:00
e443cdb35a Dark theme Toast notifications
Fixes #59
2023-10-04 00:11:42 +00:00
8c98edb604 Fix depenndencies 2023-10-03 23:48:59 +00:00
9d1bab6a07 Less dodgy debug/version handling 2023-10-03 23:48:52 +00:00
c87abb506d Add docker build and push script 2023-10-03 23:47:57 +00:00
c03f632ea0 Add useful VScode Extensions to devcontainer.json 2023-10-03 23:01:12 +00:00
ab0aca9f90 Add song info and remove string bodging 2023-10-03 22:57:37 +00:00
e3f8839c07 Fix queue modal header border 2023-10-03 22:57:03 +00:00
81267a4484 Update devcontainer.json 2023-10-03 22:56:40 +00:00
a1a041c5ce Add detailed song info view on song select 2023-10-03 19:23:09 +00:00
c3603a13dd Merge pull request #74 from PhoenixTwoFive/73-fix-code-scanning-alert-flask-app-is-run-in-debug-mode
Remove standalone run stub
2023-10-03 20:22:20 +02:00
c9613dfbd9 Remove standalone run stub 2023-10-03 18:17:36 +00:00
a1c8181779 Merge pull request #72 from PhoenixTwoFive/71-fix-code-scanning-alert-clear-text-logging-of-sensitive-information
Remove Logging
2023-10-03 19:51:06 +02:00
1ef4830588 Fix formatting issues 2023-10-03 17:46:52 +00:00
5a8b2fe66c Remove Logging 2023-10-03 17:42:21 +00:00
0db1ef1fc4 Apply Flask deprecation fixes 2023-10-03 17:36:10 +00:00
dcc79aed1b Improve search
(cherry picked from commit 85497a1569)
2023-10-03 17:25:39 +00:00
3d8cf665db Update devcontainer.json 2023-10-03 17:23:54 +00:00
8f926621c1 Merge pull request #62 from PhoenixTwoFive/dependabot/pip/backend/flask-2.3.2
Bump flask from 2.2.3 to 2.3.2 in /backend
2023-09-29 23:33:02 +02:00
a8e1a8f647 Merge pull request #67 from PhoenixTwoFive/dependabot/pip/backend/requests-2.31.0
Bump requests from 2.28.2 to 2.31.0 in /backend
2023-09-29 23:32:42 +02:00
e8e0bca648 Update devcontainer.json 2023-09-22 16:55:05 +00:00
5efa21924b Update dockerfile to adapt to new debian version 2023-06-30 15:59:04 +02:00
4c806c3550 update docker-compose container version 2023-06-30 15:58:34 +02:00
c21e6300e9 Merge pull request #70 from PhoenixTwoFive/64-defaults-for-entry-limitation-too-high
lowered maxqueue to 10 and participant quota to 2
2023-06-30 15:34:56 +02:00
7525708dce lowered maxqueue to 10 and participant quota to 2 2023-06-30 15:32:13 +02:00
37d95f61b2 Merge pull request #69 from PhoenixTwoFive/63-input-sanitization
63 input sanitization
2023-06-30 15:29:46 +02:00
a54953ff0d fix copy functionality
Fixed copy functionality by no longer rendering the data into the
function call as string literal, but instead accessing it from the DOM
in the function call.
2023-06-30 15:28:46 +02:00
349eff9a09 strip whitespace from participant name in enqueue endpoint 2023-06-30 14:50:32 +02:00
b7a79462dc Add pipfile 2023-06-30 00:17:44 +02:00
0a038029f7 Merge pull request #68 from PhoenixTwoFive/61-properly-set-focus-on-opening-enqueue-modal
Set focus on singer name input when modal is shown
2023-06-30 00:16:09 +02:00
93e6606d6d Set focus on singer name input when modal is shown 2023-06-30 00:13:48 +02:00
bd3bec8c4f Bump requests from 2.28.2 to 2.31.0 in /backend
Bumps [requests](https://github.com/psf/requests) from 2.28.2 to 2.31.0.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.28.2...v2.31.0)

---
updated-dependencies:
- dependency-name: requests
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-23 06:41:06 +00:00
9cb93d2d49 Bump flask from 2.2.3 to 2.3.2 in /backend
Bumps [flask](https://github.com/pallets/flask) from 2.2.3 to 2.3.2.
- [Release notes](https://github.com/pallets/flask/releases)
- [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/flask/compare/2.2.3...2.3.2)

---
updated-dependencies:
- dependency-name: flask
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-02 00:33:58 +00:00
1faa2614fa Merge branch 'legacy' of github.com:PhoenixTwoFive/karaoqueue into legacy 2023-04-28 13:41:51 +02:00
93c8a2cb7b Update Version 2023-04-28 13:41:39 +02:00
ddb1e0d2a0 Merge pull request #58 from PhoenixTwoFive:fix/legacy/fix-no-owned-entry
Fix error on no owned entries
2023-04-28 13:40:28 +02:00
e66e4a6c19 Fix error on no owned entries 2023-04-28 13:39:20 +02:00
08d0e5557b Update docker-compose to new version 2023-04-28 13:13:47 +02:00
a987dfc9d2 Merge branch 'legacy' of github.com:PhoenixTwoFive/karaoqueue into legacy 2023-04-28 13:03:57 +02:00
04511a91a1 Freeze versions to avoid Flask 2.3 deprecation problems 2023-04-28 13:03:53 +02:00
7da0dc19dc Merge pull request #57 from PhoenixTwoFive/feature/legacy/56-buttons-for-copying-to-clipboard-in-entry-list
Feature/legacy/56 buttons for copying to clipboard in entry list
2023-04-28 12:56:36 +02:00
d0d8e41b48 Indicate copyability of table text
Indicate copyability on click of table contents by highlighting
hovered element
2023-04-28 12:53:03 +02:00
f2b4611ea6 Remove problematic tooltip and debug log 2023-04-28 12:52:03 +02:00
b76fcfd8e4 Add Copy functionality and corresponding toasts 2023-04-28 12:40:28 +02:00
19 changed files with 1064 additions and 408 deletions

View File

@ -1,32 +1,40 @@
{
"image": "mcr.microsoft.com/devcontainers/universal:2",
"hostRequirements": {
"cpus": 4
"image": "mcr.microsoft.com/devcontainers/universal:2",
"hostRequirements": {
"cpus": 4
},
"waitFor": "onCreateCommand",
"onCreateCommand": "curl -LsS https://r.mariadb.com/downloads/mariadb_repo_setup | sudo bash && sudo apt install -y libmariadb3 libmariadb-dev",
"updateContentCommand": "pip install -r backend/requirements.txt",
"portsAttributes": {
"5000": {
"label": "Application",
"onAutoForward": "openPreview"
}
},
"customizations": {
"codespaces": {
"openFiles": [
"templates/index.html"
]
},
"waitFor": "onCreateCommand",
"updateContentCommand": "pip install -r requirements.txt",
"postCreateCommand": "",
"postAttachCommand": {
"server": "flask --debug run"
},
"portsAttributes": {
"5000": {
"label": "Application",
"onAutoForward": "openPreview"
}
},
"customizations": {
"codespaces": {
"openFiles": [
"templates/index.html"
]
},
"vscode": {
"extensions": [
"ms-python.python"
]
}
},
"forwardPorts": [5000]
}
"vscode": {
"extensions": [
"ms-python.python",
"batisteo.vscode-django",
"ms-python.flake8",
"ms-python.isort",
"ms-python.vscode-pylance",
"redhat.vscode-yaml",
"ms-azuretools.vscode-docker",
"donjayamanne.python-extension-pack"
]
}
},
"forwardPorts": [
5000
],
"features": {
"ghcr.io/devcontainers-contrib/features/angular-cli:2": {}
}
}

2
.gitignore vendored
View File

@ -87,7 +87,7 @@ ipython_config.py
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
Pipfile.lock
# celery beat schedule file
celerybeat-schedule

View File

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

View File

@ -1,16 +1,23 @@
FROM tiangolo/meinheld-gunicorn-flask:python3.9
RUN apt-key adv --recv-keys --keyserver keyserver.ubuntu.com 0xcbcb082a1bb943db
RUN curl -LsS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash
# Currently unusable, mariadb is not available through installer for debian 12
# RUN apt-key adv --recv-keys --keyserver keyserver.ubuntu.com 0xcbcb082a1bb943db
# RUN curl -LsS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash
RUN apt-get update
RUN apt-get upgrade -y
RUN apt-get dist-upgrade
# In the meantime, acquire the mariadb packages through apt
RUN apt-get install -y libmariadb3 libmariadb-dev
COPY ./backend/requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt
RUN pip install --no-cache-dir -U meinheld
ARG SOURCE_VERSION
ENV SOURCE_VERSION ${SOURCE_VERSION:-unknown}
COPY ./backend /app

48
backend/Pipfile Normal file
View File

@ -0,0 +1,48 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
autopep8 = "*"
beautifulsoup4 = "*"
bs4 = "*"
certifi = "*"
charset-normalizer = "*"
click = "*"
flake8 = "*"
flask = "*"
flask-basicauth = "*"
greenlet = "*"
gunicorn = "*"
idna = "*"
itsdangerous = "*"
jinja2 = "*"
mariadb = "*"
markupsafe = "*"
mccabe = "*"
mysql = "*"
mysqlclient = "*"
numpy = "*"
packaging = "*"
pandas = "*"
pycodestyle = "*"
pyflakes = "*"
pymysql = "*"
python-dateutil = "*"
pytz = "*"
requests = "*"
six = "*"
soupsieve = "*"
sqlalchemy = "*"
toml = "*"
tomli = "*"
typing-extensions = "*"
urllib3 = "*"
werkzeug = "*"
[dev-packages]
[requires]
python_version = "3.11"
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
from flask_basicauth import BasicAuth
from helpers import nocache
from werkzeug.utils import secure_filename
app = Flask(__name__, static_url_path='/static')
basic_auth = BasicAuth(app)
@ -16,9 +17,9 @@ accept_entries = True
@app.route("/")
def home():
if basic_auth.authenticate():
return render_template('main_admin.html', list=database.get_list(), auth=basic_auth.authenticate())
return render_template('main_admin.html', list=database.get_list(), auth=basic_auth.authenticate(), debug=app.config['DEBUG'])
else:
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(), debug=app.config['DEBUG'])
@app.route("/favicon.ico")
@ -37,7 +38,7 @@ def enqueue():
if not helpers.is_valid_uuid(client_id):
print(request.data)
abort(400)
name = request.json['name']
name = request.json['name'].strip()
song_id = request.json['id']
if request.authorization:
entry_id = database.add_entry(name, song_id, client_id)
@ -67,14 +68,14 @@ def enqueue():
@app.route("/list")
def songlist():
return render_template('songlist.html', list=database.get_song_list(), auth=basic_auth.authenticate())
return render_template('songlist.html', list=database.get_song_list(), auth=basic_auth.authenticate(), debug=app.config['DEBUG'])
@app.route("/settings")
@nocache
@basic_auth.required
def settings():
return render_template('settings.html', app=app, auth=basic_auth.authenticate(), themes=helpers.get_themes())
return render_template('settings.html', app=app, auth=basic_auth.authenticate(), themes=helpers.get_themes(), debug=app.config['DEBUG'])
@app.route("/settings", methods=['POST'])
@ -109,7 +110,7 @@ def settings_post():
if changed_credentials:
return redirect("/")
else:
return render_template('settings.html', app=app, auth=basic_auth.authenticate(), themes=helpers.get_themes())
return render_template('settings.html', app=app, auth=basic_auth.authenticate(), themes=helpers.get_themes(), debug=app.config['DEBUG'])
@app.route("/api/queue")
@ -123,7 +124,7 @@ def queue_json():
@nocache
@basic_auth.required
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(), debug=app.config['DEBUG'])
@app.route("/api/songs")
@ -157,6 +158,92 @@ def get_song_completions(input_string=""):
return 400
@app.route("/api/songs/search")
@nocache
def query_songs_with_details(input_string=""):
input_string = request.args.get("q", input_string)
if input_string == "":
return Response(status=400)
result = []
for x in database.get_songs_with_details(input_string):
# 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/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>")
def get_song_details(song_id):
result = database.get_song_details(song_id)
if result is None:
abort(404)
else:
return jsonify(dict(zip(['karafun_id', 'title', 'artist', 'year', 'duo', 'explicit', 'styles', 'languages'], result[0])))
@app.route("/api/entries/delete/<entry_id>", methods=['GET'])
@nocache
@basic_auth.required
@ -239,14 +326,20 @@ def get_accept_entries():
return Response('{"status": "OK", "value": ' + str(int(accept_entries)) + '}', mimetype='text/json')
@app.route("/api/played/clear")
@app.route("/api/event/close")
@nocache
@basic_auth.required
def clear_played_songs():
if database.clear_played_songs():
def close_event():
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')
else:
return Response('{"status": "FAIL"}', mimetype='text/json')
except Exception:
response = jsonify({"status": "FAIL", "message": "An error occured while closing the event."})
response.status_code = 400
return response
@app.route("/api/entries/delete_all")
@ -272,17 +365,18 @@ def get_current_event():
return Response('{"status": "OK", "event": "' + helpers.get_current_event_id(app) + '"}', mimetype='text/json')
@app.before_first_request
def activate_job():
helpers.load_dbconfig(app)
helpers.load_version(app)
database.create_entry_table()
database.create_song_table()
database.create_done_song_table()
database.create_list_view()
database.create_done_song_view()
database.create_config_table()
helpers.setup_config(app)
with app.app_context():
helpers.load_dbconfig(app)
helpers.load_version(app)
database.create_schema()
database.create_entry_table()
database.create_song_table()
database.create_done_song_table()
database.create_list_view()
database.create_done_song_view()
database.create_config_table()
helpers.setup_config(app)
@app.after_request
@ -301,5 +395,5 @@ def inject_version():
return dict(karaoqueue_version=app.config['VERSION'])
if __name__ == "__main__":
app.run(host='127.0.0.1', port=8080, debug=True)
# Perform setup here so it will be executed when the module is imported by the WSGI server.
activate_job()

View File

@ -28,6 +28,11 @@ def import_songs(song_csv):
with get_db_engine().connect() as conn:
df.to_sql(song_table, conn, if_exists='replace',
index=False)
try:
cur = conn.execute(text("ALTER TABLE songs ADD FULLTEXT(Title,Artist)"))
conn.commit()
except Exception:
pass
cur = conn.execute(text("SELECT Count(Id) FROM songs"))
num_songs = cur.fetchone()[0] # type: ignore
conn.commit()
@ -35,6 +40,30 @@ def import_songs(song_csv):
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():
with get_db_engine().connect() as conn:
stmt = text(
@ -62,7 +91,20 @@ def create_song_table():
`Explicit` INTEGER,
`Date Added` TIMESTAMP,
`Styles` TEXT,
`Languages` TEXT
`Languages` TEXT,
PRIMARY KEY (`Id`),
FULLTEXT KEY (`Title`,`Artist`)
)""")
conn.execute(stmt)
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()
@ -114,6 +156,34 @@ def get_played_list():
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():
with get_db_engine().connect() as conn:
stmt = text("SELECT Artist || \" - \" || Title AS Song, Id FROM songs;")
@ -123,11 +193,46 @@ def get_song_list():
def get_song_completions(input_string):
with get_db_engine().connect() as conn:
prepared_string = f"%{input_string.upper()}%"
prepared_string = f"{input_string}"
prepared_string_with_wildcard = f"%{input_string}%"
stmt = text(
"SELECT CONCAT(Artist, ' - ', Title) AS Song, Id FROM songs WHERE CONCAT(Artist, ' - ', Title) LIKE :prepared_string LIMIT 20;")
"""
SELECT CONCAT(Artist, ' - ', Title) AS Song, Id FROM songs
WHERE MATCH(Artist, Title)
AGAINST (:prepared_string IN NATURAL LANGUAGE MODE)
LIMIT 20;
""")
cur = conn.execute(
stmt, {"prepared_string": prepared_string}) # type: ignore
stmt, {"prepared_string": prepared_string, "prepared_string_with_wildcard": prepared_string_with_wildcard}) # type: ignore
return cur.fetchall()
def get_songs_with_details(input_string: str):
with get_db_engine().connect() as conn:
prepared_string = f"%{input_string}"
stmt = text(
"""
SELECT Id, Title, Artist, Year, Duo, Explicit, Styles, Languages FROM songs
WHERE MATCH(Artist, Title)
AGAINST (:prepared_string IN NATURAL LANGUAGE MODE)
LIMIT 20;
"""
)
cur = conn.execute(
stmt, {"prepared_string": prepared_string})
return cur.fetchall()
def get_song_details(song_id: int):
with get_db_engine().connect() as conn:
stmt = text(
"""
SELECT Id, Title, Artist, Year, Duo, Explicit, Styles, Languages FROM songs
WHERE Id = :song_id;
"""
)
cur = conn.execute(
stmt, {"song_id": song_id})
return cur.fetchall()
@ -182,6 +287,23 @@ def check_queue_length():
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():
with get_db_engine().connect() as conn:
conn.execute(text("DELETE FROM done_songs"))
@ -250,7 +372,6 @@ def get_config(key: str) -> str:
def set_config(key: str, value: str) -> bool:
print(f"Setting config {key} to {value}")
with get_db_engine().connect() as conn:
conn.execute(text(
"INSERT INTO config (`Key`, `Value`) VALUES ( :par_key , :par_value) ON DUPLICATE KEY UPDATE `Value`= :par_value"),

View File

@ -1,5 +1,6 @@
import requests
from bs4 import BeautifulSoup
import subprocess
import os
import uuid
from flask import make_response, Flask
@ -34,17 +35,14 @@ def check_config_exists():
def load_version(app: Flask):
if app.config['DEBUG'] is True:
app.config['VERSION'] = subprocess.Popen("echo \"$(git rev-parse --abbrev-ref HEAD)-$(git describe)\"", shell=True, stdout=subprocess.PIPE).stdout.read().decode('utf-8').strip() + " (debug)" # noqa: E501 # type: ignore
return
if os.environ.get("SOURCE_VERSION"):
app.config['VERSION'] = os.environ.get("SOURCE_VERSION")[0:7] # type: ignore # noqa: E501
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'] = ""
app.config['VERSION'] = os.environ.get("SOURCE_VERSION") # type: ignore # noqa: E501
return
else:
app.config['VERSION'] = ""
app.config['VERSION'] = "Unknown"
def load_dbconfig(app: Flask):
@ -91,8 +89,8 @@ def setup_config(app: Flask):
exit()
default_config = {'username': initial_username,
'password': initial_password,
'entryquota': 3,
'maxqueue': 20,
'entryquota': 2,
'maxqueue': 10,
'entries_allowed': 1,
'theme': 'default.css'}
for key, value in default_config.items():

View File

@ -1,30 +1,40 @@
autopep8
beautifulsoup4
bs4
certifi
charset-normalizer
click
Flask
Flask-BasicAuth
greenlet
gunicorn
idna
itsdangerous
Jinja2
mariadb
MarkupSafe
mysql
mysqlclient
numpy
pandas
pycodestyle
PyMySQL
python-dateutil
pytz
requests
six
soupsieve
SQLAlchemy
toml
urllib3
Werkzeug
autopep8==2.0.4
beautifulsoup4==4.12.3
blinker==1.7.0
bs4==0.0.2
certifi==2024.2.2
charset-normalizer==3.3.2
click==8.1.7
flake8==7.0.0
Flask==3.0.2
Flask-BasicAuth==0.2.0
greenlet==3.0.3
gunicorn==21.2.0
idna==3.6
itsdangerous==2.1.2
Jinja2==3.1.3
mariadb==1.1.10
MarkupSafe==2.1.5
mccabe==0.7.0
mysql==0.0.3
mysqlclient==2.2.4
numpy==1.26.4
packaging==23.2
pandas==2.2.0
pipfile-upgrade==0.0.2
pycodestyle==2.11.1
pyflakes==3.2.0
PyMySQL==1.1.0
python-dateutil==2.8.2
pytz==2024.1
requests==2.31.0
six==1.16.0
soupsieve==2.5
SQLAlchemy==2.0.27
toml==0.10.2
tomli==2.0.1
tomlkit==0.12.3
typing_extensions==4.9.0
tzdata==2024.1
urllib3==2.2.0
Werkzeug==3.0.1

View File

@ -1,4 +1,212 @@
: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);
--navbar-padding: 4.5rem;
/* Common */
--background-color: #ffffff;
--background-color-var: #f5f5f5;
--text-color: #212529;
--text-color-var: #343a40;
--text-color-light: #6c757d;
/* Modals */
--modal-background-color: #ffffff;
--modal-separator-color: #dee2e6;
--modal-close-color: #212529;
/* Tables */
--table-border-color: #dee2e6;
/* Input */
--input-background-color: #ffffff;
/* Misc */
--copy-highlight-color: rgba(251, 255, 0, 0.6);
/* Toasts */
--toast-background-color: #ffffff;
--toast-text-color: #212529;
/* Footer */
--footer-height: 80px;
}
body {
padding-top: var(--navbar-padding);
background-color: var(--background-color);
}
html,
body {
height: 100%;
}
.site {
min-height: calc(100vh - var(--navbar-padding) - var(--footer-height));
margin-bottom: -var(--footer-height);
}
.footer {
/*margin-top: var(--footer-height);*/
width: 100%;
height: var(--footer-height);
background-color: var(--background-color-var);
}
.topbutton {
width: 100%;
}
table td {
overflow: hidden;
text-overflow: ellipsis;
}
table.entries tbody tr[data-index="0"] {
background-color: #007bff80;
font-weight: 600;
}
table.entries tbody tr[data-index="1"] {
background-color: #007bff40;
font-weight: 500;
}
table.entries tbody tr[data-index="2"] {
background-color: #007bff20;
font-weight: 400;
}
table.entries tbody tr[data-index="3"] {
background-color: #007bff10;
}
table td:first-child {
max-width: 200px !important;
}
.fa-solid {
vertical-align: auto;
}
@media (min-width: 768px) {
.topbutton {
width: auto;
}
}
@media print {
body {
font-size: 1.3em;
}
.footer {
display: none !important;
}
.admincontrols {
display: none;
}
}
body {
background-color: var(--background-color);
color: var(--text-color);
}
.footer {
background-color: var(--background-color-var);
}
.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(--modal-separator-color);
}
.modal-footer {
background-color: var(--background-color);
color: var(--text-color-var);
border-color: 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)
}
table td.buttoncell {
text-align: end;
}
.close {
color: var(--text-color)
}
pre {
color: var(--text-color-var)
}
#entrytable td>span:hover {
background-color: var(--copy-highlight-color);
}
.list-indicator {
width: 1.5rem;
height: 1.5rem;
}
#songYear {
font-size: small;
font-weight: 400;
color: var(--text-color-light);
}
.construction_bg {
background: repeating-linear-gradient(45deg,
#222200,
#222200 10px,
#000000 10px,
#000000 20px) !important;
}
.toast {
background-color: var(--toast-background-color);
color: var(--toast-text-color);
}
.toast-header {
background-color: var(--toast-background-color);
color: var(--toast-text-color);
}
@media (prefers-color-scheme: dark) {
:root {
/* Navbar */
--navbar-background-color: #343a40;
--navbar-text-color: rgba(255, 255, 255, .5);
@ -6,189 +214,25 @@
--navbar-text-color-active: rgba(255, 255, 255, 1);
/* Common */
--background-color: #ffffff;
--background-color-var: #f5f5f5;
--text-color: #212529;
--text-color-var: #343a40;
--background-color: #121212;
--background-color-var: #232323;
--text-color: #f5f5f5;
--text-color-var: #a2a2a2;
--text-color-light: #6c757d;
/* Modals */
--modal-background-color: #ffffff;
--modal-separator-color: #dee2e6;
--modal-close-color: #212529;
--modal-background-color: #121212;
--modal-separator-color: #232323;
--modal-close-color: #f5f5f5;
/* Tables */
--table-border-color: #dee2e6;
--table-border-color: #232323;
/* Input */
--input-background-color: #ffffff;
}
--input-background-color: #343434;
body {
padding-top: 5rem;
background-color: var(--background-color);
}
html,
body {
height: 100%;
}
.site {
height: auto;
min-height: 100%;
}
main {
padding-bottom: 60px;
/* Höhe des Footers */
}
.footer {
margin-top: -60px;
width: 100%;
height: 60px;
/* Set the fixed height of the footer here */
/*line-height: 60px; /* Vertically center the text there */
background-color: var(--background-color-var);
}
.topbutton {
width: 100%;
}
table td {
overflow: hidden;
text-overflow: ellipsis;
}
table.entries tbody tr[data-index="0"] {
background-color: #007bff80;
font-weight: 600;
}
table.entries tbody tr[data-index="1"] {
background-color: #007bff40;
font-weight: 500;
}
table.entries tbody tr[data-index="2"] {
background-color: #007bff20;
font-weight: 400;
}
table.entries tbody tr[data-index="3"] {
background-color: #007bff10;
}
table td:first-child {
max-width: 200px !important;
}
.fa-solid {
vertical-align: auto;
}
@media (min-width: 768px) {
.topbutton {
width: auto;
}
}
@media print {
body {
font-size: 1.3em;
}
.footer {
display: none !important;
}
.admincontrols {
display: none;
}
}
body {
background-color: var(--background-color);
color: var(--text-color);
}
.footer {
background-color: var(--background-color-var);
}
.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)
}
table td.buttoncell {
text-align: end;
}
.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;
}
/* Toasts */
--toast-background-color: #232323;
--toast-text-color: #f5f5f5;
}
}

View File

@ -15,9 +15,15 @@
<!-- Bootstrap-Tables -->
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.21.2/dist/bootstrap-table.min.css">
<!-- Bootstrap-Toaster-->
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-toaster/4.1.2/css/bootstrap-toaster.min.css"
integrity="sha512-kYPLvO+Bu+xttOhbQvxs9nx7XSdxrb2JexRxQ3CpJQ7EtmlkBsWyOjlinLgiLWeLxuupFYB4cPqLOo0gnBnzeQ=="
crossorigin="anonymous" referrerpolicy="no-referrer" />
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.2.1/dist/css/bootstrap.min.css"
integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" crossorigin="anonymous">
<!-- Custom styles for this template -->
<link href="static/css/style.css" rel="stylesheet">
@ -73,31 +79,31 @@
</main><!-- /.container -->
</div>
<!-- Footer -->
<footer class="footer">
<footer class="footer {% if debug %} construction_bg {% endif %}">
<div class="container text-center py-3">
{% if not auth %}
<a href="/login" class="ml-1 mr-1"><i class="fas fa-sign-in-alt mr-1"></i><span>Login</span></a>
{% 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_version}} -&nbsp;2019-23 - Phillip
Kühne</span>
<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 - <span id="myName">Phillip
Kühne</span></span>
</div>
</footer>
<!-- Footer -->
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="https://code.jquery.com/jquery-3.2.1.min.js"
integrity="sha384-xBuQ/xzmlsLoJpyjoggmTEz8OWUFM0/RC5BsqQBDX2v5cMvDHcMakNTNrHIW2I5f" crossorigin="anonymous">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.4/jquery.min.js"
integrity="sha512-pumBsjNRGGqkPzKHndZMaAG+bir374sORyzM3uulLV14lN5LyykqNk8eEeUlUkB3U0M4FApyaHraT65ihJhDpQ=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"
integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous">
</script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"
integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous">
</script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.14.6/dist/umd/popper.min.js"
integrity="sha384-wHAiFfRlMFy6i5SRaxvfOCifBUQy1xHdJ/yoi7FRNXMRBu5WHdZYu1hA6ZOblgut"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.2.1/dist/js/bootstrap.min.js"
integrity="sha384-B0UglyR+jN6CkvvICOB2joaf5I4l3gm9GU6Hc1og6Ls7i6U/mkkaduKaBhlAXv9k"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/4.4.0/bootbox.min.js"
integrity="sha256-4F7e4JsAJyLUdpP7Q8Sah866jCOhv72zU5E8lIRER4w=" crossorigin="anonymous">
</script>
@ -105,6 +111,10 @@
<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>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-toaster/4.1.2/js/bootstrap-toaster.min.js"
integrity="sha512-Ur6jgeoP3jnn38C7oBzDqMLRb+wxG2PXLKqgx2vgQ1ePFvbJ28f9iQSJplHD0APFHELOeS/df+RPNeENFtLrYw=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
{% block extrajs %}{% endblock %}
<script>
$(document).ready(function () {
@ -113,6 +123,9 @@
var pathname = window.location.pathname;
$('.navbar-nav > li > a[href="' + pathname + '"]').parent().addClass('active');
$('[data-toggle="tooltip"]').tooltip()
setTimeout(function () {
$('#myName').text("Klack-Klack")
}, Math.floor(Math.random() * 20000) + 20000);
})
function create_UUID() {
@ -149,25 +162,29 @@
}
function addEntry(entryId) {
entryArray = JSON.parse(localStorage.getItem("ownedEntries"))
if (entryArray == null) {
entryArray = []
}
entryArray.push(entryId)
localStorage.setItem("ownedEntries", JSON.stringify(entryArray))
entryArray = JSON.parse(localStorage.getItem("ownedEntries"))
if (entryArray == null) {
entryArray = []
}
entryArray.push(entryId)
localStorage.setItem("ownedEntries", JSON.stringify(entryArray))
}
function removeEntry(entryId) {
entryArray = JSON.parse(localStorage.getItem("ownedEntries"))
if (entryArray == null) {
entryArray = []
}
entryArray = entryArray.filter(function(value, index, arr){ return value != entryId;});
localStorage.setItem("ownedEntries", JSON.stringify(entryArray))
entryArray = JSON.parse(localStorage.getItem("ownedEntries"))
if (entryArray == null) {
entryArray = []
}
entryArray = entryArray.filter(function (value, index, arr) { return value != entryId; });
localStorage.setItem("ownedEntries", JSON.stringify(entryArray))
}
function getOwnedEntries() {
return JSON.parse(localStorage.getItem("ownedEntries"))
var entries = JSON.parse(localStorage.getItem("ownedEntries"))
if (entries == null) {
entries = []
}
return entries;
}
</script>

View File

@ -1,5 +1,3 @@
{% extends 'base.html' %}
{% block title %}Warteliste{% endblock %}
{% block content %}
@ -9,7 +7,7 @@
data-url="/api/queue"
data-pagination="true"
data-classes="table"
data-show-refresh="false"
data-show-refresh="false"
data-auto-refresh="true"
data-auto-refresh-interval="10">
<thead>
@ -25,47 +23,51 @@
{% endblock %}
{% block extrajs %}
<script>
$.getJSON("/api/entries/accept", (data) => {
if (data["value"]==0) {
$("#bfb").addClass("disabled")
$("#bfb").prop("aria-disabled",true);
$("#bfb").prop("tabindex","-1");
$("#bfb").wrap("<span class='tooltip-span' tabindex='0' data-toggle='tooltip' data-placement='bottom'></span>");
$(".tooltip-span").prop("title", "Eintragungen sind leider momentan nicht möglich.")
$('[data-toggle="tooltip"]').tooltip()
}
})
function TableActionsFormatter(value,row,index) {
console.log("Value: " + value + ", Row: " + row + ", Index: " + index)
console.log(row)
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 ""
}
function requestDeletionAsUser(id) {
bootbox.confirm("Wirklich den Eintrag zurückziehen? Das könnte zu einer langen Wartezeit führen!", function (result) {
if (result) {
payload = {
"client_id": localStorage.getItem("clientId"),
"entry_id": id
$.getJSON("/api/entries/accept", (data) => {
if (data["value"] == 0) {
$("#bfb").addClass("disabled")
$("#bfb").prop("aria-disabled", true);
$("#bfb").prop("tabindex", "-1");
$("#bfb").wrap("<span class='tooltip-span' tabindex='0' data-toggle='tooltip' data-placement='bottom'></span>");
$(".tooltip-span").prop("title", "Eintragungen sind leider momentan nicht möglich.")
$('[data-toggle="tooltip"]').tooltip()
}
$.ajax({
url: "/api/entries/delete/"+id,
type: "POST",
data: JSON.stringify(payload),
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function(result) {
bootbox.alert("Eintrag zurückgezogen!")
location.reload()
})
function TableActionsFormatter(value, row, index) {
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 ""
}
function requestDeletionAsUser(id) {
bootbox.confirm("Wirklich den Eintrag zurückziehen? Das könnte zu einer langen Wartezeit führen!", function (result) {
if (result) {
payload = {
"client_id": localStorage.getItem("clientId"),
"entry_id": id
}
$.ajax({
url: "/api/entries/delete/" + id,
type: "POST",
data: JSON.stringify(payload),
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (result) {
toast = {
title: "Erfolgreich zurückgezogen",
message: "Eintrag wurde gelöscht",
status: TOAST_STATUS.SUCCESS,
timeout: 5000
}
Toast.create(toast);
location.reload()
}
})
}
})
}
})
}
</script>
{% endblock %}

View File

@ -26,9 +26,9 @@
<thead>
<tr>
<th data-field="state" data-checkbox="true"></th>
<th scope="col" data-field="Name">Name</th>
<th scope="col" data-field="Title">Song</th>
<th scope="col" data-field="Artist">Künstler</th>
<th scope="col" data-field="Name" data-formatter="CopyFormatter">Name</th>
<th scope="col" data-field="Title" data-formatter="CopyFormatter">Song</th>
<th scope="col" data-field="Artist" data-formatter="CopyFormatter">Künstler</th>
<th scope="col" data-formatter="TableActions">Aktionen</th>
</tr>
</thead>
@ -185,21 +185,36 @@
});
}
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\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Markierung zurückziehen\" onclick=\"event.stopPropagation();$(this).tooltip('dispose');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=\"event.stopPropagation();$(this).tooltip('dispose');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=\"event.stopPropagation();$(this).tooltip('dispose');confirmDeleteEntry('" + row.Name + "'," + row.entry_ID + ")\"><i class=\"fas fa-trash\"></i></button>";
outerHTML = "<button type=\"button\" class=\"btn btn-default\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Markierung zurückziehen\" onclick=\"event.stopPropagation();$(this).tooltip('dispose');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=\"event.stopPropagation();$(this).tooltip('dispose');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=\"event.stopPropagation();$(this).tooltip('dispose');confirmDeleteEntry(this.parentElement.parentElement.children[1].innerText," + row.entry_ID + ")\"><i class=\"fas fa-trash\"></i></button>";
} else {
outerHTML = "<button type=\"button\" class=\"btn btn-info\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Als übertragen markieren\" onclick=\"event.stopPropagation();$(this).tooltip('dispose');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=\"event.stopPropagation();$(this).tooltip('dispose');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=\"event.stopPropagation();$(this).tooltip('dispose');confirmDeleteEntry('" + row.Name + "'," + row.entry_ID + ")\"><i class=\"fas fa-trash\"></i></button>";
outerHTML = "<button type=\"button\" class=\"btn btn-info\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Als übertragen markieren\" onclick=\"event.stopPropagation();$(this).tooltip('dispose');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=\"event.stopPropagation();$(this).tooltip('dispose');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=\"event.stopPropagation();$(this).tooltip('dispose');confirmDeleteEntry(this.parentElement.parentElement.children[1].innerText," + row.entry_ID + ")\"><i class=\"fas fa-trash\"></i></button>";
}
return outerHTML;
}
function CopyFormatter(value, row, index) {
let escapedString = value.replace("\"", "\\\"").replace("\'", "\\\'")
return "<span onclick='copyAndNotify(this.innerText)'>" + value + "</span>";
}
function getIdSelections() {
return $.map($("#entrytable").bootstrapTable('getSelections'), function (row) {
return row.entry_ID
})
}
function copyAndNotify(text) {
navigator.clipboard.writeText(text).then(function () {
let toast = {
title: "Kopiert",
message: text,
status: 1,
timeout: 5000
};
Toast.create(toast);
})
}
</script>
{% endblock %}

View File

@ -2,8 +2,8 @@
{% block title %}Abspielliste{% endblock %}
{% block content %}
<div id="toolbar">
<button type="button" class="topbutton btn btn-danger" onclick="confirmDeleteAllEntries()"><i
class="fas fa-trash mr-2"></i>Abspielliste löschen</button>
<button type="button" class="topbutton btn btn-danger" onclick="confirmCloseEvent()"><i
class="fas fa-trash mr-2"></i>Event beenden</button>
<button type="button" class="topbutton btn btn-primary" onclick="exportPDF()"><i
class="fas fa-file-pdf mr-2"></i>Als PDF herunterladen</button>
<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://unpkg.com/jspdf-autotable@3.0.10/dist/jspdf.plugin.autotable.js"></script>
<script>
function confirmDeleteAllEntries() {
function confirmCloseEvent() {
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: {
confirm: {
label: 'Ja',
@ -56,15 +64,15 @@
},
callback: function(result){
if (result) {
deleteAllEntries()
closeEvent()
}
}
})
}
function deleteAllEntries() {
function closeEvent() {
$.ajax({
type: 'GET',
url: '/api/played/clear',
url: '/api/event/close',
contentType: "application/json",
dataType: 'json',
async: false

View File

@ -4,7 +4,8 @@
<form method="post">
<p>
<label for="entryquota">Maximale Anzahl an Einträgen pro Nutzer</label>
<input type="number" class="form-control" id="entryquota" name="entryquota" min=1 value={{app.config['ENTRY_QUOTA']}}>
<input type="number" class="form-control" id="entryquota" name="entryquota" min=1
value={{app.config['ENTRY_QUOTA']}}>
</p>
<p>
<label for="maxqueue">Maximale Anzahl an Einträgen Insgesamt</label>
@ -14,7 +15,7 @@
<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>
<option value="{{theme}}" {% if theme==config['THEME'] %}selected{% endif %}>{{theme}}</option>
{% endfor %}
</select>
</p>
@ -24,7 +25,8 @@
</div>
<p>
<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>
<label for="password">Passwort ändern</label>
@ -32,10 +34,69 @@
</p>
<input type="submit" class="btn btn-primary mr-1 mb-2" value="Einstellungen anwenden">
</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>
<summary>Current config:</summary>
<pre>{% for key, val in config.items() %}{{key}}: {{val}}<br>{% endfor %}</pre>
</details>
{% endblock %}
{% 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 %}

View File

@ -1,7 +1,8 @@
{% extends 'base.html' %}
{% block title %}Songsuche{% endblock %}
{% 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">
<tbody id="songtable">
</tbody>
@ -11,21 +12,52 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Auf Liste setzen</h5>
<h5 class="modal-title" id="exampleModalLabel">Auf die Liste setzen</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="songinfo container">
<div class="row">
<div class="col">
<h5 id="songTitle"></h5>
<p><span id="songArtist"></span>&nbsp;<span id="songYear"></span></p>
</div>
</div>
<div class="row">
<div class="col-1">
<p><i class="fas fa-info"></i></p>
</div>
<div class="col" id="indicators">
</div>
</div>
<div class="row">
<div class="col-1">
<p><i class="fas fa-file-audio"></i></p>
</div>
<div class="col">
<p id="songGenres"></p>
</div>
</div>
<div class="row">
<div class="col-1">
<p><i class="fas fa-language"></i></p>
</div>
<div class="col">
<p id="songLanguages"></p>
</div>
</div>
</div>
<div class="form-group">
<form id="nameForm">
<div class="modal-body">
<label for="singerNameInput">Sängername</label>
<label for="singerNameInput">Dein Name:</label>
<input type="text" class="form-control" id="singerNameInput" placeholder="Max Mustermann"
required>
<input id="selectedId" name="selectedId" type="hidden" value="">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Schließen</button>
<button type="submit" class="btn btn-primary" id="submitSongButton">Anmelden</button>
</div>
</form>
@ -37,38 +69,79 @@
{% block extrajs %}
<script>
$(document).ready(function () {
$("#filter").focus();
$("#filter").keyup(function () {
var value = $(this).val().toLowerCase();
//alert(value);
if (value.length >= 1) {
$.getJSON("/api/songs/compl", { search: value }, function (data) {
var items = [];
$.each(data, function (key, val) {
items.push("<tr><td>" + val[0] + `</td>
<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
class="fas fa-plus"></i></button></td>
</tr>`)
});
$("#songtable").html("")
$(items.join("")).appendTo("#songtable");
entriesAccepted()
});
} else {
$("#songtable").html("")
}
});
getSuggestions(10);
$("#filter").keyup(debounce(() => songSearch()));
$("#nameForm").submit(function (e) {
e.preventDefault();
submitModal();
});
$('#enqueueModal').on('shown.bs.modal', function (e) {
$("#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) {
var data = {
@ -90,25 +163,109 @@
}
function setSelectedId(id) {
$("#songArtist").html("");
$("#songTitle").html("");
$("#songYear").html("");
$("#indicators")[0].innerHTML = "";
$("#selectedId").attr("value", id);
$.getJSON("/api/songs/details/" + id, function (data) {
$("#songTitle").html(data["title"]);
$("#songArtist").html(data["artist"]);
$("#songYear").html(data["year"]);
$("#indicators")[0].innerHTML = "";
let duoindicator_badge = document.createElement("span");
duoindicator_badge.classList.add("badge");
duoindicator_badge.classList.add("badge-secondary");
duoindicator_badge.classList.add("badge-pill");
duoindicator_badge.classList.add("mx-1");
duoindicator_badge.classList.add("p-2");
if (data["duo"] == 0) {
duoindicator_badge.innerHTML = "Solo";
let duoindicator = document.createElement("i");
duoindicator.classList.add("fas");
duoindicator.classList.add("fa-user");
duoindicator.classList.add("ml-1");
duoindicator_badge.appendChild(duoindicator);
$("#indicators")[0].appendChild(duoindicator_badge)
}
if (data["duo"] == 1) {
duoindicator_badge.innerHTML = "Duo";
let duoindicator = document.createElement("i");
duoindicator.classList.add("fas");
duoindicator.classList.add("fa-user-friends");
duoindicator.classList.add("ml-1");
duoindicator_badge.appendChild(duoindicator);
$("#indicators")[0].appendChild(duoindicator_badge)
}
if (data["explicit"] == 1) {
let explicitindicator_badge = document.createElement("span");
explicitindicator_badge.classList.add("badge");
explicitindicator_badge.classList.add("badge-secondary");
explicitindicator_badge.classList.add("badge-pill");
explicitindicator_badge.classList.add("mx-1");
explicitindicator_badge.classList.add("p-2");
explicitindicator_badge.innerHTML = "Explicit";
let explicitindicator = document.createElement("i");
explicitindicator.classList.add("fas");
explicitindicator.classList.add("fa-e");
explicitindicator.classList.add("ml-1");
explicitindicator_badge.appendChild(explicitindicator);
$("#indicators")[0].appendChild(explicitindicator_badge)
}
let styles = data["styles"].split(",");
let languages = data["languages"].split(",");
$("#songGenres").html("");
$("#songLanguages").html("");
for (let i = 0; i < styles.length; i++) {
let badge = document.createElement("span");
badge.classList.add("badge");
badge.classList.add("badge-secondary");
badge.classList.add("badge-pill");
badge.classList.add("mx-1");
badge.classList.add("p-2");
badge.innerHTML = styles[i];
$("#songGenres")[0].appendChild(badge);
}
for (let i = 0; i < languages.length; i++) {
let badge = document.createElement("span");
badge.classList.add("badge");
badge.classList.add("badge-secondary");
badge.classList.add("badge-pill");
badge.classList.add("mx-1");
badge.classList.add("p-2");
badge.innerHTML = languages[i];
$("#songLanguages")[0].appendChild(badge);
}
});
}
function submitModal() {
var name = $("#singerNameInput").val();
var id = $("#selectedId").attr("value");
enqueue(localStorage.getItem("clientId"),id, name, function (response) {
console.log(response);
enqueue(localStorage.getItem("clientId"), id, name, function (response) {
entryID = response["entry_id"];
bootbox.alert({
message: "Deine Eintragung wurde erfolgreich vorgenommen.",
});
console.log("Entry ID: " + entryID);
toast = {
title: "Erfolgreich eingetragen",
message: "Du wurdest erfolgreich eingetragen.",
status: TOAST_STATUS.SUCCESS,
timeout: 5000
}
Toast.create(toast);
addEntry(entryID);
$("#enqueueModal").modal('hide');
window.location.href = '/#end';
}, function (response) {
bootbox.alert({
message: "Deine Eintragung konnte leider nicht vorgenommen werden.\nGrund: "+response.responseJSON.status,
message: "Deine Eintragung konnte leider nicht vorgenommen werden.\nGrund: " + response.responseJSON.status,
});
entriesAccepted();
$("#enqueueModal").modal('hide');
@ -118,6 +275,36 @@
}
function songSearch() {
let value = $("#filter").val()
if (value.length >= 1) {
$("#suggestionExplainer").hide();
$.getJSON("/api/songs/search", { q: value }, function (data) {
var items = [];
$("#songtable").html("")
$.each(data, function (key, val) {
$("#songtable").append(constructResultRow(val))
});
$(items.join("")).appendTo("#songtable");
entriesAccepted()
});
} else {
$("#songtable").html("")
getSuggestions(10);
}
}
function debounce(func, timeout = 300) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
};
}
{% if not auth %}
function entriesAccepted() {
$.getJSON("/api/entries/accept", (data, out) => {
@ -129,6 +316,7 @@
$('[data-toggle="tooltip"]').tooltip()
} else {
$(".enqueueButton").prop("disabled", false)
}
})

39
build_container.sh Executable file
View File

@ -0,0 +1,39 @@
#!/bin/bash
# Get username from command line
if [ $# -eq 0 ]; then
echo "No username supplied. Please supply a github username as the first argument."
exit 1
fi
# Store username in variable
USERNAME=$1
# Check for uncommitted changes
if ! git diff-index --quiet HEAD --; then
echo "You have uncommitted changes. Please commit or stash them and try again."
exit 1
fi
# Get the appropriate version of the container using git
VERSION=$(git rev-parse --abbrev-ref HEAD)-$(git describe)
# Build the container. Add the version as a tag and as ENV variable SOURCE_VERSION
docker build -t ghcr.io/$USERNAME/karaoqueue:$VERSION --build-arg SOURCE_VERSION=$VERSION .
# Ask the user if they want to push the container. Confirm Version.
read -p "Push container to ghcr.io/$USERNAME/karaoqueue:$VERSION? [y/n] " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]
then
docker push ghcr.io/$USERNAME/karaoqueue:$VERSION
fi
# Ask the user if they want to push the container as latest
read -p "Push container to ghcr.io/$USERNAME/karaoqueue:latest? [y/n] " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]
then
docker tag ghcr.io/$USERNAME/karaoqueue:$VERSION ghcr.io/$USERNAME/karaoqueue:latest
docker push ghcr.io/$USERNAME/karaoqueue:latest
fi

View File

@ -6,7 +6,7 @@ secrets:
services:
karaoqueue:
image: "ghcr.io/phoenixtwofive/karaoqueue:v2023.03.3"
image: "ghcr.io/phoenixtwofive/karaoqueue:v2023.06"
build: .
restart: always
ports: