Compare commits

...

75 Commits

Author SHA1 Message Date
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
c9cbd24569 Merge pull request #55 from PhoenixTwoFive/feature/legacy/53-löschung-von-eigenen-einträgen-erlauben
Feature/legacy/53 löschung von eigenen einträgen erlauben
2023-04-27 00:18:37 +02:00
add528fb80 Löschung eigener Enträge implementiert. 2023-04-26 19:20:21 +02:00
865df5d588 Implement EventID to scope ClientIDs and Entry IDs
Implement an EventID saved in settings. Currently this is used to scope
clientIDs and entryIDs to an event. The client checks the event currently going on on
the server, and discards its localstorage (containing the clientID) if
it has changed
2023-04-26 18:08:03 +02:00
adebf35d08 Update lint.yaml
Remove action run on every push
2023-04-25 16:52:36 +02:00
d2caaac4bc Codecheck (#54)
* Add GitHub Action

* Add Linting

* Add .editorconfig
2023-04-25 16:46:43 +02:00
58dd0dd93b Add devcontainer.json setup 2023-04-02 17:21:56 +02:00
10717e753b Update Compose File 2023-03-31 20:58:47 +02:00
f32f02dc44 Fix Sorting in List view 2023-03-31 20:57:34 +02:00
3921a9ea76 Update docker-Compose 2023-03-30 22:56:09 +02:00
8c735866a3 Remove remnants of file based config 2023-03-30 22:45:08 +02:00
035394c36b Rephrase Information text 2023-03-30 22:37:39 +02:00
1d4f77ea19 Merge pull request #49 from PhoenixTwoFive/PhoenixTwoFive/issue45
Inform user that entries are closed when big button on main page is pressed and disabled
2023-03-30 22:34:52 +02:00
573f58d764 Inform user that entries are closed when big button on main page is pressed and disabled
Fixes #45
2023-03-30 22:33:50 +02:00
dff46404f6 Merge pull request #48 from PhoenixTwoFive:bugfix/47-misaligned-buttons
Fix button alignment
2023-03-30 22:28:57 +02:00
21 changed files with 1023 additions and 351 deletions

View File

@ -0,0 +1,40 @@
{
"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"
]
},
"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": {}
}
}

36
.editorconfig Normal file
View File

@ -0,0 +1,36 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false
[*.md]
trim_trailing_whitespace = true
[*.py]
indent_size = 4
[*.js]
indent_size = 2
[*.html]
indent_size = 2
[*.css]
indent_size = 2
[*.scss]
indent_size = 2
[*.yaml]
indent_size = 2
[*.yml]
indent_size = 2

3
.flake8 Normal file
View File

@ -0,0 +1,3 @@
[flake8]
ignore = E501
max-line-length = 120

23
.github/workflows/lint.yaml vendored Normal file
View File

@ -0,0 +1,23 @@
name: Lint
on: [pull_request]
jobs:
flake8_py3:
runs-on: ubuntu-latest
steps:
- name: Setup Python
uses: actions/setup-python@v4.6.0
with:
python-version: '3.10'
architecture: x64
- name: Checkout PyTorch
uses: actions/checkout@master
- name: Install flake8
run: pip install flake8
- name: Run flake8
uses: suo/flake8-github-action@releases/v1
with:
checkName: 'flake8_py3' # NOTE: this needs to be the same as the job name
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

2
.gitignore vendored
View File

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

16
.vscode/settings.json vendored
View File

@ -1,3 +1,17 @@
{ {
"python.pythonPath": "/usr/bin/python" "python.pythonPath": "/usr/bin/python",
"python.testing.unittestArgs": [
"-v",
"-s",
"./backend/tests",
"-p",
"*test.py"
],
"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 FROM tiangolo/meinheld-gunicorn-flask:python3.9
RUN apt-key adv --recv-keys --keyserver keyserver.ubuntu.com 0xcbcb082a1bb943db # Currently unusable, mariadb is not available through installer for debian 12
RUN curl -LsS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash # 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 update
RUN apt-get upgrade -y RUN apt-get upgrade -y
RUN apt-get dist-upgrade 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 COPY ./backend/requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt
RUN pip install --no-cache-dir -U meinheld RUN pip install --no-cache-dir -U meinheld
ARG SOURCE_VERSION
ENV SOURCE_VERSION ${SOURCE_VERSION:-unknown}
COPY ./backend /app 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 = "==2.0.2"
beautifulsoup4 = "==4.12.0"
bs4 = "==0.0.1"
certifi = "==2022.12.7"
charset-normalizer = "==3.1.0"
click = "==8.1.3"
flake8 = "==6.0.0"
flask = "==2.2.3"
flask-basicauth = "==0.2.0"
greenlet = "==2.0.2"
gunicorn = "==20.1.0"
idna = "==3.4"
itsdangerous = "==2.1.2"
jinja2 = "==3.1.2"
mariadb = "==1.1.6"
markupsafe = "==2.1.2"
mccabe = "==0.7.0"
mysql = "==0.0.3"
mysqlclient = "==2.1.1"
numpy = "==1.24.2"
packaging = "==23.0"
pandas = "==1.5.3"
pycodestyle = "==2.10.0"
pyflakes = "==3.0.1"
pymysql = "==1.0.3"
python-dateutil = "==2.8.2"
pytz = "==2023.3"
requests = "==2.28.2"
six = "==1.16.0"
soupsieve = "==2.4"
sqlalchemy = "==2.0.7"
toml = "==0.10.2"
tomli = "==2.0.1"
typing-extensions = "==4.5.0"
urllib3 = "==1.26.15"
werkzeug = "==2.2.3"
[dev-packages]
[requires]
python_version = "3.9"
python_full_version = "3.9.17"

View File

@ -1,5 +1,5 @@
from flask import Flask, render_template, abort, request, redirect, send_from_directory, jsonify from flask import Flask, render_template, abort, request, redirect, send_from_directory, jsonify
from flask.wrappers import Request, Response from flask.wrappers import Response
import helpers import helpers
import database import database
import data_adapters import data_adapters
@ -12,12 +12,13 @@ app = Flask(__name__, static_url_path='/static')
basic_auth = BasicAuth(app) basic_auth = BasicAuth(app)
accept_entries = True accept_entries = True
@app.route("/") @app.route("/")
def home(): def home():
if basic_auth.authenticate(): 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: 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") @app.route("/favicon.ico")
@ -36,11 +37,11 @@ def enqueue():
if not helpers.is_valid_uuid(client_id): if not helpers.is_valid_uuid(client_id):
print(request.data) print(request.data)
abort(400) abort(400)
name = request.json['name'] name = request.json['name'].strip()
song_id = request.json['id'] song_id = request.json['id']
if request.authorization: if request.authorization:
database.add_entry(name, song_id, client_id) entry_id = database.add_entry(name, song_id, client_id)
return Response('{"status":"OK"}', mimetype='text/json') return Response(f"""{{"status":"OK", "entry_id":{entry_id}}}""", mimetype='text/json')
else: else:
if helpers.get_accept_entries(app): if helpers.get_accept_entries(app):
if not request.json: if not request.json:
@ -54,8 +55,8 @@ def enqueue():
song_id = request.json['id'] song_id = request.json['id']
if database.check_queue_length() < int(app.config['MAX_QUEUE']): if database.check_queue_length() < int(app.config['MAX_QUEUE']):
if database.check_entry_quota(client_id) < int(app.config['ENTRY_QUOTA']): if database.check_entry_quota(client_id) < int(app.config['ENTRY_QUOTA']):
database.add_entry(name, song_id, client_id) entry_id = database.add_entry(name, song_id, client_id)
return Response('{"status":"OK"}', mimetype='text/json') return Response(f"""{{"status":"OK", "entry_id":{entry_id}}}""", mimetype='text/json')
else: else:
return Response('{"status":"Du hast bereits ' + str(database.check_entry_quota(client_id)) + ' Songs eingetragen, dies ist das Maximum an Einträgen die du in der Warteliste haben kannst."}', mimetype='text/json', status=423) return Response('{"status":"Du hast bereits ' + str(database.check_entry_quota(client_id)) + ' Songs eingetragen, dies ist das Maximum an Einträgen die du in der Warteliste haben kannst."}', mimetype='text/json', status=423)
else: else:
@ -66,14 +67,14 @@ def enqueue():
@app.route("/list") @app.route("/list")
def songlist(): 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") @app.route("/settings")
@nocache @nocache
@basic_auth.required @basic_auth.required
def settings(): 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']) @app.route("/settings", methods=['POST'])
@ -95,7 +96,7 @@ def settings_post():
else: else:
abort(400) abort(400)
if theme is not None and theme in helpers.get_themes(): if theme is not None and theme in helpers.get_themes():
helpers.set_theme(app,theme) helpers.set_theme(app, theme)
else: else:
abort(400) abort(400)
if username != "" and username != app.config['BASIC_AUTH_USERNAME']: if username != "" and username != app.config['BASIC_AUTH_USERNAME']:
@ -108,7 +109,7 @@ def settings_post():
if changed_credentials: if changed_credentials:
return redirect("/") return redirect("/")
else: 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") @app.route("/api/queue")
@ -122,7 +123,7 @@ def queue_json():
@nocache @nocache
@basic_auth.required @basic_auth.required
def played_list(): def played_list():
return render_template('played_list.html', list=database.get_played_list(), auth=basic_auth.authenticate()) return render_template('played_list.html', list=database.get_played_list(), auth=basic_auth.authenticate(), debug=app.config['DEBUG'])
@app.route("/api/songs") @app.route("/api/songs")
@ -137,18 +138,18 @@ def songs():
@basic_auth.required @basic_auth.required
def update_songs(): def update_songs():
database.delete_all_entries() database.delete_all_entries()
helpers.reset_current_event_id(app)
status = database.import_songs( status = database.import_songs(
helpers.get_songs(helpers.get_catalog_url())) helpers.get_songs(helpers.get_catalog_url()))
print(status) print(status)
return Response('{"status": "%s" }' % status, mimetype='text/json') return Response('{"status": "%s" }' % status, mimetype='text/json')
@app.route("/api/songs/compl") # type: ignore @app.route("/api/songs/compl") # type: ignore
@nocache @nocache
def get_song_completions(input_string=""): def get_song_completions(input_string=""):
input_string = request.args.get('search', input_string) input_string = request.args.get('search', input_string)
if input_string != "": if input_string != "":
print(input_string)
result = [list(x) for x in database.get_song_completions(input_string=input_string)] result = [list(x) for x in database.get_song_completions(input_string=input_string)]
return jsonify(result) return jsonify(result)
@ -156,10 +157,51 @@ def get_song_completions(input_string=""):
return 400 return 400
@app.route("/api/entries/delete/<entry_id>") @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/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 @nocache
@basic_auth.required @basic_auth.required
def delete_entry(entry_id): def delete_entry_admin(entry_id):
if database.delete_entry(entry_id):
return Response('{"status": "OK"}', mimetype='text/json')
else:
return Response('{"status": "FAIL"}', mimetype='text/json')
@app.route("/api/entries/delete/<entry_id>", methods=['POST'])
@nocache
def delete_entry_user(entry_id):
if not request.json:
print(request.data)
abort(400)
client_id = request.json['client_id']
if not helpers.is_valid_uuid(client_id):
print(request.data)
abort(400)
if database.get_raw_entry(entry_id)[3] != client_id: # type: ignore
print(request.data)
abort(403)
if database.delete_entry(entry_id): if database.delete_entry(entry_id):
return Response('{"status": "OK"}', mimetype='text/json') return Response('{"status": "OK"}', mimetype='text/json')
else: else:
@ -176,7 +218,7 @@ def delete_entries():
return return
updates = database.delete_entries(request.json) updates = database.delete_entries(request.json)
if updates >= 0: if updates >= 0:
return Response('{"status": "OK", "updates": '+str(updates)+'}', mimetype='text/json') return Response('{"status": "OK", "updates": ' + str(updates) + '}', mimetype='text/json')
else: else:
return Response('{"status": "FAIL"}', mimetype='text/json', status=400) return Response('{"status": "FAIL"}', mimetype='text/json', status=400)
@ -190,6 +232,7 @@ def mark_sung(entry_id):
else: else:
return Response('{"status": "FAIL"}', mimetype='text/json') return Response('{"status": "FAIL"}', mimetype='text/json')
@app.route("/api/entries/mark_transferred/<entry_id>") @app.route("/api/entries/mark_transferred/<entry_id>")
@nocache @nocache
@basic_auth.required @basic_auth.required
@ -205,7 +248,7 @@ def mark_transferred(entry_id):
@basic_auth.required @basic_auth.required
def set_accept_entries(value): def set_accept_entries(value):
if (value == '0' or value == '1'): if (value == '0' or value == '1'):
helpers.set_accept_entries(app,bool(int(value))) helpers.set_accept_entries(app, bool(int(value)))
return Response('{"status": "OK"}', mimetype='text/json') return Response('{"status": "OK"}', mimetype='text/json')
else: else:
return Response('{"status": "FAIL"}', mimetype='text/json', status=400) return Response('{"status": "FAIL"}', mimetype='text/json', status=400)
@ -215,7 +258,7 @@ def set_accept_entries(value):
@nocache @nocache
def get_accept_entries(): def get_accept_entries():
accept_entries = helpers.get_accept_entries(app) accept_entries = helpers.get_accept_entries(app)
return Response('{"status": "OK", "value": '+str(int(accept_entries))+'}', mimetype='text/json') return Response('{"status": "OK", "value": ' + str(int(accept_entries)) + '}', mimetype='text/json')
@app.route("/api/played/clear") @app.route("/api/played/clear")
@ -233,6 +276,7 @@ def clear_played_songs():
@basic_auth.required @basic_auth.required
def delete_all_entries(): def delete_all_entries():
if database.delete_all_entries(): if 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: else:
return Response('{"status": "FAIL"}', mimetype='text/json') return Response('{"status": "FAIL"}', mimetype='text/json')
@ -244,20 +288,24 @@ def admin():
return redirect("/", code=303) return redirect("/", code=303)
@app.before_first_request @app.route("/api/events/current")
def activate_job(): @nocache
helpers.load_dbconfig(app) def get_current_event():
helpers.load_version(app) return Response('{"status": "OK", "event": "' + helpers.get_current_event_id(app) + '"}', mimetype='text/json')
helpers.create_data_directory()
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)
def activate_job():
with app.app_context():
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)
@app.after_request @app.after_request
def add_header(response): def add_header(response):
@ -265,14 +313,15 @@ def add_header(response):
Add headers to both force latest IE rendering engine or Chrome Frame, Add headers to both force latest IE rendering engine or Chrome Frame,
and also to cache the rendered page for 10 minutes. and also to cache the rendered page for 10 minutes.
""" """
if not 'Cache-Control' in response.headers: if 'Cache-Control' not in response.headers:
response.headers['Cache-Control'] = 'private, max-age=600, no-cache, must-revalidate' response.headers['Cache-Control'] = 'private, max-age=600, no-cache, must-revalidate'
return response return response
@app.context_processor @app.context_processor
def inject_version(): def inject_version():
return dict(karaoqueue_version=app.config['VERSION']) return dict(karaoqueue_version=app.config['VERSION'])
if __name__ == "__main__": # Perform setup here so it will be executed when the module is imported by the WSGI server.
app.run(host='127.0.0.1', port=8080, debug=True) activate_job()

View File

@ -1,5 +1,5 @@
def dict_from_rows(rows): def dict_from_rows(rows):
outlist=[] outlist = []
for row in rows: for row in rows:
outlist.append(dict(row._mapping)) outlist.append(dict(row._mapping))
return outlist return outlist

View File

@ -1,11 +1,10 @@
# -*- coding: utf_8 -*- # -*- coding: utf_8 -*-
from email.mime import base
from MySQLdb import Connection
from sqlalchemy import create_engine, engine, text from sqlalchemy import create_engine, engine, text
import pandas import pandas
from io import StringIO from io import StringIO
from flask import current_app from flask import current_app
import uuid
song_table = "songs" song_table = "songs"
entry_table = "entries" entry_table = "entries"
@ -18,7 +17,6 @@ sql_engine = None
def get_db_engine() -> engine.base.Engine: def get_db_engine() -> engine.base.Engine:
global sql_engine global sql_engine
if (not sql_engine): if (not sql_engine):
print(current_app.config.get("DBCONNSTRING"))
sql_engine = create_engine( sql_engine = create_engine(
current_app.config.get("DBCONNSTRING")) # type: ignore current_app.config.get("DBCONNSTRING")) # type: ignore
return sql_engine return sql_engine
@ -30,6 +28,11 @@ def import_songs(song_csv):
with get_db_engine().connect() as conn: with get_db_engine().connect() as conn:
df.to_sql(song_table, conn, if_exists='replace', df.to_sql(song_table, conn, if_exists='replace',
index=False) 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")) cur = conn.execute(text("SELECT Count(Id) FROM songs"))
num_songs = cur.fetchone()[0] # type: ignore num_songs = cur.fetchone()[0] # type: ignore
conn.commit() conn.commit()
@ -64,7 +67,9 @@ def create_song_table():
`Explicit` INTEGER, `Explicit` INTEGER,
`Date Added` TIMESTAMP, `Date Added` TIMESTAMP,
`Styles` TEXT, `Styles` TEXT,
`Languages` TEXT `Languages` TEXT,
PRIMARY KEY (`Id`),
FULLTEXT KEY (`Title`,`Artist`)
)""") )""")
conn.execute(stmt) conn.execute(stmt)
conn.commit() conn.commit()
@ -75,7 +80,9 @@ def create_list_view():
stmt = text("""CREATE OR REPLACE VIEW `Liste` AS stmt = text("""CREATE OR REPLACE VIEW `Liste` AS
SELECT Name, Title, Artist, entries.Id AS entry_ID, songs.Id AS song_ID, entries.Transferred SELECT Name, Title, Artist, entries.Id AS entry_ID, songs.Id AS song_ID, entries.Transferred
FROM entries, songs FROM entries, songs
WHERE entries.Song_Id=songs.Id""") WHERE entries.Song_Id=songs.Id
ORDER BY entries.Id ASC
""")
conn.execute(stmt) conn.execute(stmt)
conn.commit() conn.commit()
@ -123,22 +130,57 @@ def get_song_list():
def get_song_completions(input_string): def get_song_completions(input_string):
with get_db_engine().connect() as conn: 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( 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( 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() return cur.fetchall()
def add_entry(name, song_id, client_id): def add_entry(name, song_id, client_id):
with get_db_engine().connect() as conn: with get_db_engine().connect() as conn:
stmt = text( stmt = text(
"INSERT INTO entries (Song_Id,Name,Client_Id) VALUES (:par_song_id,:par_name,:par_client_id);") "INSERT INTO entries (Song_Id,Name,Client_Id) VALUES (:par_song_id,:par_name,:par_client_id) RETURNING entries.ID;")
conn.execute(stmt, {"par_song_id": song_id, "par_name": name, cur = conn.execute(stmt, {"par_song_id": song_id, "par_name": name,
"par_client_id": client_id}) # type: ignore "par_client_id": client_id}) # type: ignore
conn.commit() conn.commit()
return True return cur.fetchone()[0] # type: ignore
def add_sung_song(entry_id): def add_sung_song(entry_id):
@ -189,6 +231,26 @@ def clear_played_songs():
return True return True
def get_entry(id):
try:
with get_db_engine().connect() as conn:
cur = conn.execute(text("SELECT * FROM Liste WHERE entry_ID = :par_id"),
{"par_id": id}) # type: ignore
return cur.fetchall()[0]
except Exception:
return None
def get_raw_entry(id):
try:
with get_db_engine().connect() as conn:
cur = conn.execute(text("SELECT * FROM entries WHERE ID = :par_id"),
{"par_id": id}) # type: ignore
return cur.fetchall()[0]
except Exception:
return None
def delete_entry(id): def delete_entry(id):
with get_db_engine().connect() as conn: with get_db_engine().connect() as conn:
conn.execute(text("DELETE FROM entries WHERE id= :par_id"), { conn.execute(text("DELETE FROM entries WHERE id= :par_id"), {
@ -207,7 +269,7 @@ def delete_entries(ids):
"par_id": idlist}) "par_id": idlist})
conn.commit() conn.commit()
return cur.rowcount return cur.rowcount
except Exception as error: except Exception:
return -1 return -1
@ -225,12 +287,11 @@ def get_config(key: str) -> str:
text("SELECT `Value` FROM config WHERE `Key`= :par_key"), {"par_key": key}) # type: ignore text("SELECT `Value` FROM config WHERE `Key`= :par_key"), {"par_key": key}) # type: ignore
conn.commit() conn.commit()
return cur.fetchall()[0][0] return cur.fetchall()[0][0]
except IndexError as error: except IndexError:
return "" return ""
def set_config(key: str, value: str) -> bool: def set_config(key: str, value: str) -> bool:
print(f"Setting config {key} to {value}")
with get_db_engine().connect() as conn: with get_db_engine().connect() as conn:
conn.execute(text( conn.execute(text(
"INSERT INTO config (`Key`, `Value`) VALUES ( :par_key , :par_value) ON DUPLICATE KEY UPDATE `Value`= :par_value"), "INSERT INTO config (`Key`, `Value`) VALUES ( :par_key , :par_value) ON DUPLICATE KEY UPDATE `Value`= :par_value"),
@ -254,9 +315,24 @@ def check_config_table() -> bool:
if conn.dialect.has_table(conn, 'config'): if conn.dialect.has_table(conn, 'config'):
# type: ignore # type: ignore
# type: ignore # type: ignore
if (conn.execute(text("SELECT COUNT(*) FROM config")).fetchone()[0] > 0): # type: ignore if (conn.execute(text("SELECT COUNT(*) FROM config")).fetchone()[0] > 0): # type: ignore
return True return True
else: else:
return False return False
else: else:
return False return False
def init_event_id() -> bool:
if not get_config("EventID"):
set_config("EventID", str(uuid.uuid4()))
return True
def reset_event_id() -> bool:
set_config("EventID", str(uuid.uuid4()))
return True
def get_event_id() -> str:
return get_config("EventID")

View File

@ -1,6 +1,6 @@
import requests import requests
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import json import subprocess
import os import os
import uuid import uuid
from flask import make_response, Flask from flask import make_response, Flask
@ -8,14 +8,6 @@ from functools import wraps, update_wrapper
from datetime import datetime from datetime import datetime
import database import database
data_directory = "data"
config_file = data_directory+"/config.json"
def create_data_directory():
if not os.path.exists(data_directory):
os.makedirs(data_directory)
def get_catalog_url(): def get_catalog_url():
r = requests.get('https://www.karafun.de/karaoke-song-list.html') r = requests.get('https://www.karafun.de/karaoke-song-list.html')
@ -43,17 +35,14 @@ def check_config_exists():
def load_version(app: Flask): 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"): if os.environ.get("SOURCE_VERSION"):
app.config['VERSION'] = os.environ.get("SOURCE_VERSION")[0:7] # type: ignore # noqa: E501 app.config['VERSION'] = os.environ.get("SOURCE_VERSION") # type: ignore # noqa: E501
elif os.path.isfile(".version"): return
with open('.version', 'r') as file:
data = file.read().replace('\n', '')
if data:
app.config['VERSION'] = data
else:
app.config['VERSION'] = ""
else: else:
app.config['VERSION'] = "" app.config['VERSION'] = "Unknown"
def load_dbconfig(app: Flask): def load_dbconfig(app: Flask):
@ -78,13 +67,15 @@ def load_dbconfig(app: Flask):
else: else:
app.config['DBCONNSTRING'] = "" app.config['DBCONNSTRING'] = ""
else: else:
exit("No database connection string found. Cannot continue. Please set the environment variable DBSTRING or create a file .dbconn in the root directory of the project.") exit("""No database connection string found. Cannot continue.
Please set the environment variable DBSTRING or
create a file .dbconn in the root directory of the project.""")
# Check if config exists in DB, if not, create it. # Check if config exists in DB, if not, create it.
def setup_config(app: Flask): def setup_config(app: Flask):
if check_config_exists() == False: if check_config_exists() is False:
print("No config found, creating new config") print("No config found, creating new config")
initial_username = os.environ.get("INITIAL_USERNAME") initial_username = os.environ.get("INITIAL_USERNAME")
initial_password = os.environ.get("INITIAL_PASSWORD") initial_password = os.environ.get("INITIAL_PASSWORD")
@ -98,13 +89,14 @@ def setup_config(app: Flask):
exit() exit()
default_config = {'username': initial_username, default_config = {'username': initial_username,
'password': initial_password, 'password': initial_password,
'entryquota': 3, 'entryquota': 2,
'maxqueue': 20, 'maxqueue': 10,
'entries_allowed': 1, 'entries_allowed': 1,
'theme': 'default.css'} 'theme': 'default.css'}
for key, value in default_config.items(): for key, value in default_config.items():
database.set_config(key, value) database.set_config(key, value)
print("Created new config") print("Created new config")
database.init_event_id()
config = database.get_config_list() config = database.get_config_list()
app.config['BASIC_AUTH_USERNAME'] = config['username'] app.config['BASIC_AUTH_USERNAME'] = config['username']
app.config['BASIC_AUTH_PASSWORD'] = config['password'] app.config['BASIC_AUTH_PASSWORD'] = config['password']
@ -112,6 +104,7 @@ def setup_config(app: Flask):
app.config['MAX_QUEUE'] = config['maxqueue'] app.config['MAX_QUEUE'] = config['maxqueue']
app.config['ENTRIES_ALLOWED'] = bool(config['entries_allowed']) app.config['ENTRIES_ALLOWED'] = bool(config['entries_allowed'])
app.config['THEME'] = config['theme'] app.config['THEME'] = config['theme']
app.config['EVENT_ID'] = database.get_event_id()
# set queue admittance # set queue admittance
@ -160,6 +153,15 @@ def set_theme(app: Flask, theme: str):
print("Theme not found, not setting theme.") print("Theme not found, not setting theme.")
def get_current_event_id(app: Flask):
return app.config['EVENT_ID']
def reset_current_event_id(app: Flask):
database.reset_event_id()
app.config['EVENT_ID'] = database.get_event_id()
def nocache(view): def nocache(view):
@wraps(view) @wraps(view)
def no_cache(*args, **kwargs): def no_cache(*args, **kwargs):

View File

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

View File

@ -1,30 +1,36 @@
autopep8 autopep8~=2.0.2
beautifulsoup4 beautifulsoup4~=4.12.0
bs4 bs4~=0.0.1
certifi certifi~=2022.12.7
charset-normalizer charset-normalizer~=3.1.0
click click~=8.1.3
Flask flake8~=6.0.0
Flask-BasicAuth Flask~=2.3.2
greenlet Flask-BasicAuth~=0.2.0
gunicorn greenlet~=2.0.2
idna gunicorn~=20.1.0
itsdangerous idna~=3.4
Jinja2 itsdangerous~=2.1.2
mariadb Jinja2~=3.1.2
MarkupSafe mariadb~=1.1.6
mysql MarkupSafe~=2.1.2
mysqlclient mccabe~=0.7.0
numpy mysql~=0.0.3
pandas mysqlclient~=2.1.1
pycodestyle numpy~=1.24.2
PyMySQL packaging~=23.0
python-dateutil pandas~=1.5.3
pytz pycodestyle~=2.10.0
requests pyflakes~=3.0.1
six PyMySQL~=1.0.3
soupsieve python-dateutil~=2.8.2
SQLAlchemy pytz~=2023.3
toml requests~=2.31.0
urllib3 six~=1.16.0
Werkzeug soupsieve~=2.4
SQLAlchemy~=2.0.7
toml~=0.10.2
tomli~=2.0.1
typing_extensions~=4.5.0
urllib3~=1.26.15
Werkzeug~=3.0.0

View File

@ -1,4 +1,212 @@
:root { :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 */
--navbar-background-color: #343a40; --navbar-background-color: #343a40;
--navbar-text-color: rgba(255, 255, 255, .5); --navbar-text-color: rgba(255, 255, 255, .5);
@ -6,189 +214,25 @@
--navbar-text-color-active: rgba(255, 255, 255, 1); --navbar-text-color-active: rgba(255, 255, 255, 1);
/* Common */ /* Common */
--background-color: #ffffff; --background-color: #121212;
--background-color-var: #f5f5f5; --background-color-var: #232323;
--text-color: #212529; --text-color: #f5f5f5;
--text-color-var: #343a40; --text-color-var: #a2a2a2;
--text-color-light: #6c757d;
/* Modals */ /* Modals */
--modal-background-color: #ffffff; --modal-background-color: #121212;
--modal-separator-color: #dee2e6; --modal-separator-color: #232323;
--modal-close-color: #212529; --modal-close-color: #f5f5f5;
/* Tables */ /* Tables */
--table-border-color: #dee2e6; --table-border-color: #232323;
/* Input */ /* Input */
--input-background-color: #ffffff; --input-background-color: #343434;
}
/* Toasts */
body { --toast-background-color: #232323;
padding-top: 5rem; --toast-text-color: #f5f5f5;
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;
}
} }

View File

@ -15,9 +15,15 @@
<!-- 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.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 --> <!-- Bootstrap core CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.2.1/dist/css/bootstrap.min.css"
integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous"> integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" 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">
@ -73,13 +79,13 @@
</main><!-- /.container --> </main><!-- /.container -->
</div> </div>
<!-- Footer --> <!-- Footer -->
<footer class="footer"> <footer class="footer {% if debug %} construction_bg {% endif %}">
<div class="container text-center py-3"> <div class="container text-center py-3">
{% if not auth %} {% 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> <a href="/login" class="ml-1 mr-1"><i class="fas fa-sign-in-alt mr-1"></i><span>Login</span></a>
{% endif %} {% endif %}
<a href="https://github.com/PhoenixTwoFive/karaoqueue" <a href="https://github.com/PhoenixTwoFive/karaoqueue" class="ml-1 mr-1"><i
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 - Phillip
Kühne</span> Kühne</span>
</div> </div>
@ -88,16 +94,16 @@
<!-- Bootstrap core JavaScript <!-- Bootstrap core JavaScript
================================================== --> ================================================== -->
<!-- Placed at the end of the document so the pages load faster --> <!-- Placed at the end of the document so the pages load faster -->
<script src="https://code.jquery.com/jquery-3.2.1.min.js" <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.4/jquery.min.js"
integrity="sha384-xBuQ/xzmlsLoJpyjoggmTEz8OWUFM0/RC5BsqQBDX2v5cMvDHcMakNTNrHIW2I5f" crossorigin="anonymous"> integrity="sha512-pumBsjNRGGqkPzKHndZMaAG+bir374sORyzM3uulLV14lN5LyykqNk8eEeUlUkB3U0M4FApyaHraT65ihJhDpQ=="
</script> crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" <script src="https://cdn.jsdelivr.net/npm/popper.js@1.14.6/dist/umd/popper.min.js"
integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"> integrity="sha384-wHAiFfRlMFy6i5SRaxvfOCifBUQy1xHdJ/yoi7FRNXMRBu5WHdZYu1hA6ZOblgut"
</script> crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.2.1/dist/js/bootstrap.min.js"
integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"> integrity="sha384-B0UglyR+jN6CkvvICOB2joaf5I4l3gm9GU6Hc1og6Ls7i6U/mkkaduKaBhlAXv9k"
</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>
@ -105,10 +111,14 @@
<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.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://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 %} {% block extrajs %}{% endblock %}
<script> <script>
$(document).ready(function () { $(document).ready(function () {
loadOrGenerateClientId() checkEventID()
// get current URL path and assign 'active' class // get current URL path and assign 'active' class
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');
@ -130,6 +140,50 @@
localStorage.setItem("clientId", create_UUID()) localStorage.setItem("clientId", create_UUID())
} }
} }
function getClientId() {
return localStorage.getItem("clientId")
}
async function checkEventID() {
const localEventID = localStorage.getItem("eventID")
const resp = await fetch("/api/events/current")
const respJson = await resp.json()
const remoteEventID = respJson.event
if (localEventID == null || localEventID != remoteEventID) {
localStorage.clear()
localStorage.setItem("eventID", remoteEventID)
loadOrGenerateClientId()
}
}
function addEntry(entryId) {
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))
}
function getOwnedEntries() {
var entries = JSON.parse(localStorage.getItem("ownedEntries"))
if (entries == null) {
entries = []
}
return entries;
}
</script> </script>
</body> </body>

View File

@ -1,5 +1,3 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}Warteliste{% endblock %} {% block title %}Warteliste{% endblock %}
{% block content %} {% block content %}
@ -9,7 +7,7 @@
data-url="/api/queue" data-url="/api/queue"
data-pagination="true" data-pagination="true"
data-classes="table" data-classes="table"
data-show-refresh="false" data-show-refresh="false"
data-auto-refresh="true" data-auto-refresh="true"
data-auto-refresh-interval="10"> data-auto-refresh-interval="10">
<thead> <thead>
@ -17,6 +15,7 @@
<th data-field="Name">Name</th> <th data-field="Name">Name</th>
<th data-field="Title">Song</th> <th data-field="Title">Song</th>
<th data-field="Artist">Künstler</th> <th data-field="Artist">Künstler</th>
<th scope="col" data-formatter="TableActionsFormatter"></th>
</tr> </tr>
</thead> </thead>
</table> </table>
@ -24,12 +23,53 @@
{% endblock %} {% endblock %}
{% block extrajs %} {% block extrajs %}
<script> <script>
$.getJSON("/api/entries/accept", (data) => { $.getJSON("/api/entries/accept", (data) => {
if (data["value"]==0) { if (data["value"] == 0) {
$("#bfb").addClass("disabled") $("#bfb").addClass("disabled")
$("#bfb").prop("aria-disabled",true); $("#bfb").prop("aria-disabled", true);
$("#bfb").prop("tabindex","-1"); $("#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
}
$.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> </script>
{% endblock %} {% endblock %}

View File

@ -26,9 +26,9 @@
<thead> <thead>
<tr> <tr>
<th data-field="state" data-checkbox="true"></th> <th data-field="state" data-checkbox="true"></th>
<th scope="col" data-field="Name">Name</th> <th scope="col" data-field="Name" data-formatter="CopyFormatter">Name</th>
<th scope="col" data-field="Title">Song</th> <th scope="col" data-field="Title" data-formatter="CopyFormatter">Song</th>
<th scope="col" data-field="Artist">Künstler</th> <th scope="col" data-field="Artist" data-formatter="CopyFormatter">Künstler</th>
<th scope="col" data-formatter="TableActions">Aktionen</th> <th scope="col" data-formatter="TableActions">Aktionen</th>
</tr> </tr>
</thead> </thead>
@ -185,21 +185,36 @@
}); });
} }
function TableActions(value, row, index) { function TableActions(value, row, index) {
console.log("Value: " + value + ", Row: " + row + ", Index: " + index)
console.log(row)
let outerHTML = "" let outerHTML = ""
if (row.Transferred == 1) { 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 { } 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; return outerHTML;
} }
function CopyFormatter(value, row, index) {
let escapedString = value.replace("\"", "\\\"").replace("\'", "\\\'")
return "<span onclick='copyAndNotify(this.innerText)'>" + value + "</span>";
}
function getIdSelections() { function getIdSelections() {
return $.map($("#entrytable").bootstrapTable('getSelections'), function (row) { return $.map($("#entrytable").bootstrapTable('getSelections'), function (row) {
return row.entry_ID 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> </script>
{% endblock %} {% endblock %}

View File

@ -11,21 +11,52 @@
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <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"> <button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </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"> <div class="form-group">
<form id="nameForm"> <form id="nameForm">
<div class="modal-body"> <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" <input type="text" class="form-control" id="singerNameInput" placeholder="Max Mustermann"
required> required>
<input id="selectedId" name="selectedId" type="hidden" value=""> <input id="selectedId" name="selectedId" type="hidden" value="">
</div> </div>
<div class="modal-footer"> <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> <button type="submit" class="btn btn-primary" id="submitSongButton">Anmelden</button>
</div> </div>
</form> </form>
@ -38,35 +69,15 @@
<script> <script>
$(document).ready(function () { $(document).ready(function () {
$("#filter").focus(); $("#filter").focus();
$("#filter").keyup(function () { $("#filter").keyup(debounce(() => songSearch()));
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("")
}
});
$("#nameForm").submit(function (e) { $("#nameForm").submit(function (e) {
e.preventDefault(); e.preventDefault();
submitModal(); submitModal();
}); });
$('#enqueueModal').on('shown.bs.modal', function (e) {
$("#singerNameInput").focus();
})
}); });
@ -79,7 +90,7 @@
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: '/api/enqueue', url: '/api/enqueue',
data: JSON.stringify(data), // or JSON.stringify ({name: 'jonas'}), data: JSON.stringify(data),
success: success_callback, success: success_callback,
statusCode: { statusCode: {
423: blocked_callback 423: blocked_callback
@ -90,18 +101,112 @@
} }
function setSelectedId(id) { function setSelectedId(id) {
$("#songArtist").html("");
$("#songTitle").html("");
$("#songYear").html("");
$("#indicators")[0].innerHTML = "";
$("#selectedId").attr("value", id); $("#selectedId").attr("value", id);
$.getJSON("/api/songs/details/" + id, function (data) {
console.log(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() { function submitModal() {
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 () { enqueue(localStorage.getItem("clientId"), id, name, function (response) {
console.log(response);
entryID = response["entry_id"];
toast = {
title: "Erfolgreich eingetragen",
message: "Du wurdest erfolgreich eingetragen.",
status: TOAST_STATUS.SUCCESS,
timeout: 5000
}
Toast.create(toast);
console.log("Entry ID: " + entryID);
addEntry(entryID);
$("#enqueueModal").modal('hide'); $("#enqueueModal").modal('hide');
window.location.href = '/#end'; window.location.href = '/#end';
}, function (response) { }, function (response) {
bootbox.alert({ 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(); entriesAccepted();
$("#enqueueModal").modal('hide'); $("#enqueueModal").modal('hide');
@ -111,6 +216,76 @@
} }
function songSearch() {
let value = $("#filter").val()
if (value.length >= 1) {
$.getJSON("/api/songs/search", { q: value }, function (data) {
var items = [];
$("#songtable").html("")
$.each(data, function (key, val) {
let itemRow = document.createElement("tr")
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)
});
$(items.join("")).appendTo("#songtable");
entriesAccepted()
});
} else {
$("#songtable").html("")
}
}
function debounce(func, timeout = 300) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
};
}
{% if not auth %} {% if not auth %}
function entriesAccepted() { function entriesAccepted() {
$.getJSON("/api/entries/accept", (data, out) => { $.getJSON("/api/entries/accept", (data, out) => {
@ -118,10 +293,11 @@
$(".enqueueButton").prop("disabled", true) $(".enqueueButton").prop("disabled", true)
$(".enqueueButton").prop("style", "pointer-events: none;") $(".enqueueButton").prop("style", "pointer-events: none;")
$(".enqueueButton").wrap("<span class='tooltip-span' tabindex='0' data-toggle='tooltip' data-placement='top'></span>"); $(".enqueueButton").wrap("<span class='tooltip-span' tabindex='0' data-toggle='tooltip' data-placement='top'></span>");
$(".tooltip-span").prop("title", "Eintragungen sind leider nicht mehr möglich.") $(".tooltip-span").prop("title", "Eintragungen sind leider momentan nicht möglich.")
$('[data-toggle="tooltip"]').tooltip() $('[data-toggle="tooltip"]').tooltip()
} else { } else {
$(".enqueueButton").prop("disabled", false) $(".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: services:
karaoqueue: karaoqueue:
image: "ghcr.io/phoenixtwofive/karaoqueue:v2023.03.1" image: "ghcr.io/phoenixtwofive/karaoqueue:v2023.06"
build: . build: .
restart: always restart: always
ports: ports: