Compare commits

..

190 Commits

Author SHA1 Message Date
phillip cc4c538e91 Merge pull request #84 from PhoenixTwoFive/83-dark-mode-is-broken
Fix dark mode styling
2024-03-15 15:51:05 +01:00
phillip a03c1c41bb Fix linter complaint.
Remove unused variable assignment on SQL Query execution.
2024-03-15 15:47:39 +01:00
phillip ed09a93b3d Fix dark mode styling
Fixes "Dark mode is broken" #83
2024-03-15 15:43:30 +01:00
phillip 0234d64bf2 Remove debug prints 2024-02-16 14:10:35 +01:00
phillip eb86575332 Update footer year 2024-02-16 13:20:57 +01:00
phillip 1cf6a6a733 Merge pull request #82 from PhoenixTwoFive/81-stuck-tooltips
Fix 81 stuck tooltips
2024-02-16 12:34:10 +01:00
phillip 06594855d6 Fix stuck tooltips
Fix stuck tooltips by implementing custom data  request function, and destroying existing tooltips on new data load.

Fixes #81
2024-02-16 12:32:37 +01:00
phillip 43a12228ae Update used libraries 2024-02-16 12:30:28 +01:00
phillip 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
phillip 6f1628546e Update requirements.txt accordingly. 2024-02-13 18:31:08 +01:00
Phillip Kühne 28d965ee41 Unpin dependencies in Pipfile to keep updated to latest version. 2024-02-13 18:14:03 +01:00
Phillip Kühne 68c5771fc6 Clean up old config 2024-02-13 17:48:17 +01:00
phillip 538f0a3c23 Do not generate EventID during create_schema() 2023-12-13 18:53:33 +00:00
phillip 601a062eac Heh. 2023-10-06 16:41:08 +00:00
Phillip Kühne 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
phillip d1b865f16f Remove unnecessary debug logs 2023-10-06 16:19:07 +00:00
phillip d03b46100c Remove Information exposure. 2023-10-06 15:59:21 +00:00
phillip 6d2941cfca Statistik Import- und Export. 2023-10-06 15:46:50 +00:00
phillip a1da421ffe Add suggestions 2023-10-04 18:37:08 +00:00
Phillip Kühne d8899267c3 Merge pull request #76 from PhoenixTwoFive/PhoenixTwoFive-patch-1
Update devcontainer.json
2023-10-04 15:19:02 +02:00
Phillip Kühne dd83d6c9c4 Update devcontainer.json 2023-10-04 15:18:54 +02:00
phillip 14630b97be Move mariabd install to postCreate to fix github codespaces 2023-10-04 13:11:43 +00:00
phillip 7f4625a062 Fix CSS quirks 2023-10-04 12:49:06 +00:00
phillip e7d9816010 Add song year to detail view 2023-10-04 12:48:50 +00:00
phillip 37990e596c Add python extensions to devcontainer.json 2023-10-04 12:48:28 +00:00
phillip 00e090ec48 Remove faulty postAttachCommand from devcontainer.json 2023-10-04 10:45:36 +00:00
phillip e7b0f5f2dc make requirements.txt useful 2023-10-04 01:33:35 +00:00
phillip c32ed395d8 Improve construction tape gimmick 2023-10-04 01:23:21 +00:00
phillip 95be876a19 Change terminology 2023-10-04 01:06:16 +00:00
phillip 0f9ad4f91a Fix trailing whitespace 2023-10-04 01:02:30 +00:00
phillip ac940ded8c Add debouncing for the search 2023-10-04 00:53:03 +00:00
phillip c8b65e4433 Ensure Index exists after Song import 2023-10-04 00:33:15 +00:00
phillip c50f00c1d3 Fix env var version 2023-10-04 00:24:26 +00:00
phillip 3b4152f89f Fix build script 2023-10-04 00:19:14 +00:00
phillip 4561f5f376 Merge branch 'legacy' of github.com:PhoenixTwoFive/karaoqueue into legacy 2023-10-04 00:18:33 +00:00
phillip 3728e282e3 Fix container build script 2023-10-04 00:18:29 +00:00
Phillip Kühne 6d3ca87869 Merge pull request #75 from PhoenixTwoFive/59-dark-theme-toast-notifications
Dark theme Toast notifications
2023-10-04 02:15:48 +02:00
phillip e443cdb35a Dark theme Toast notifications
Fixes #59
2023-10-04 00:11:42 +00:00
phillip 8c98edb604 Fix depenndencies 2023-10-03 23:48:59 +00:00
phillip 9d1bab6a07 Less dodgy debug/version handling 2023-10-03 23:48:52 +00:00
phillip c87abb506d Add docker build and push script 2023-10-03 23:47:57 +00:00
phillip c03f632ea0 Add useful VScode Extensions to devcontainer.json 2023-10-03 23:01:12 +00:00
phillip ab0aca9f90 Add song info and remove string bodging 2023-10-03 22:57:37 +00:00
phillip e3f8839c07 Fix queue modal header border 2023-10-03 22:57:03 +00:00
phillip 81267a4484 Update devcontainer.json 2023-10-03 22:56:40 +00:00
phillip a1a041c5ce Add detailed song info view on song select 2023-10-03 19:23:09 +00:00
Phillip Kühne 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
phillip c9613dfbd9 Remove standalone run stub 2023-10-03 18:17:36 +00:00
Phillip Kühne 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
phillip 1ef4830588 Fix formatting issues 2023-10-03 17:46:52 +00:00
phillip 5a8b2fe66c Remove Logging 2023-10-03 17:42:21 +00:00
phillip 0db1ef1fc4 Apply Flask deprecation fixes 2023-10-03 17:36:10 +00:00
phillip dcc79aed1b Improve search
(cherry picked from commit 85497a1569)
2023-10-03 17:25:39 +00:00
phillip 3d8cf665db Update devcontainer.json 2023-10-03 17:23:54 +00:00
Phillip Kühne 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
Phillip Kühne 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
phillip e8e0bca648 Update devcontainer.json 2023-09-22 16:55:05 +00:00
phillip 5efa21924b Update dockerfile to adapt to new debian version 2023-06-30 15:59:04 +02:00
phillip 4c806c3550 update docker-compose container version 2023-06-30 15:58:34 +02:00
Phillip Kühne 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
phillip 7525708dce lowered maxqueue to 10 and participant quota to 2 2023-06-30 15:32:13 +02:00
Phillip Kühne 37d95f61b2 Merge pull request #69 from PhoenixTwoFive/63-input-sanitization
63 input sanitization
2023-06-30 15:29:46 +02:00
phillip 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
phillip 349eff9a09 strip whitespace from participant name in enqueue endpoint 2023-06-30 14:50:32 +02:00
phillip b7a79462dc Add pipfile 2023-06-30 00:17:44 +02:00
Phillip Kühne 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
phillip 93e6606d6d Set focus on singer name input when modal is shown 2023-06-30 00:13:48 +02:00
dependabot[bot] 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
dependabot[bot] 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
phillip 1faa2614fa Merge branch 'legacy' of github.com:PhoenixTwoFive/karaoqueue into legacy 2023-04-28 13:41:51 +02:00
phillip 93c8a2cb7b Update Version 2023-04-28 13:41:39 +02:00
Phillip Kühne 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
phillip e66e4a6c19 Fix error on no owned entries 2023-04-28 13:39:20 +02:00
phillip 08d0e5557b Update docker-compose to new version 2023-04-28 13:13:47 +02:00
phillip a987dfc9d2 Merge branch 'legacy' of github.com:PhoenixTwoFive/karaoqueue into legacy 2023-04-28 13:03:57 +02:00
phillip 04511a91a1 Freeze versions to avoid Flask 2.3 deprecation problems 2023-04-28 13:03:53 +02:00
Phillip Kühne 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
phillip 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
phillip f2b4611ea6 Remove problematic tooltip and debug log 2023-04-28 12:52:03 +02:00
phillip b76fcfd8e4 Add Copy functionality and corresponding toasts 2023-04-28 12:40:28 +02:00
Phillip Kühne 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
phillip add528fb80 Löschung eigener Enträge implementiert. 2023-04-26 19:20:21 +02:00
phillip 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
Phillip Kühne adebf35d08 Update lint.yaml
Remove action run on every push
2023-04-25 16:52:36 +02:00
Phillip Kühne d2caaac4bc Codecheck (#54)
* Add GitHub Action

* Add Linting

* Add .editorconfig
2023-04-25 16:46:43 +02:00
Phillip Kühne 58dd0dd93b Add devcontainer.json setup 2023-04-02 17:21:56 +02:00
phillip 10717e753b Update Compose File 2023-03-31 20:58:47 +02:00
phillip f32f02dc44 Fix Sorting in List view 2023-03-31 20:57:34 +02:00
phillip 3921a9ea76 Update docker-Compose 2023-03-30 22:56:09 +02:00
phillip 8c735866a3 Remove remnants of file based config 2023-03-30 22:45:08 +02:00
phillip 035394c36b Rephrase Information text 2023-03-30 22:37:39 +02:00
Phillip Kühne 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
phillip 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
Phillip Kühne dff46404f6 Merge pull request #48 from PhoenixTwoFive:bugfix/47-misaligned-buttons
Fix button alignment
2023-03-30 22:28:57 +02:00
phillip 4fd58bc39f Fix button alignment 2023-03-30 22:28:12 +02:00
phillip bf97e9e5e4 Fix container version 2023-03-30 22:21:42 +02:00
phillip e08041338a Add persistent volume to mariadb container. 2023-03-30 21:18:40 +02:00
phillip 4c64144f3d Fix sticky Popups 2023-03-30 21:05:36 +02:00
phillip fad21ba6c5 Fix DELETE statements 2023-03-30 19:30:27 +02:00
phillip 3f09b79844 Fix Dockerfile for "new" WSGI server 2023-03-30 19:30:13 +02:00
phillip 1bfe3b5d4b Update Dockerfile 2023-03-30 19:14:24 +02:00
phillip f055a59a38 Fix Dockerfile with in-Container update 2023-03-30 18:14:40 +02:00
phillip dc53d8a8b1 Fix Typos 2023-03-30 18:00:23 +02:00
phillip fe71fa2d8c Fix Caching 2023-03-30 18:00:08 +02:00
phillip 7ef938a5ff Create new config with credentials from env vars 2023-03-30 17:51:20 +02:00
phillip 12207c1246 Admin credentials can be changed via settings 2023-03-30 17:39:44 +02:00
phillip 16cb9e7d5a Fix theme setting persistence 2023-03-30 17:39:13 +02:00
phillip 429ffddced Update data_adapters for sqlalchemy upgrade 2023-03-30 17:38:34 +02:00
phillip ca73d57567 Update Requirements 2023-03-30 17:07:01 +02:00
phillip cf6d586856 Overhaul database code 2023-03-30 17:06:52 +02:00
phillip 24458a78d0 Fix bugs 2023-03-30 17:06:36 +02:00
phillip a2cf4fc47f Change .env.dev to reflect new settings storage 2023-03-30 17:05:46 +02:00
phillip e84ff1a381 Change container registry to github 2023-03-30 17:05:17 +02:00
phillip 79156c539a Better Dark Mode 2023-03-30 04:43:53 +02:00
phillip 757bfa2483 Fix Dark Mode 2023-03-30 03:28:23 +02:00
phillip d56eb609b9 Fix entry button alignment 2023-03-30 03:25:02 +02:00
phillip 6d9541f0bd Add Dark Mode 2023-03-30 03:24:45 +02:00
phillip ee4774159c Update Python to 3.10 2023-03-30 03:23:22 +02:00
phillip 6d084ee83c Add theming 2023-03-30 02:57:21 +02:00
phillip 971548189f Remove redundant save button from settings 2023-03-30 02:26:24 +02:00
phillip da8ad57293 Fix Settings 2023-03-30 02:22:59 +02:00
phillip 98981b1e1e Move config to database 2023-03-30 02:22:46 +02:00
phillip d33006251e Remove inline env from docker-compose.prod.yml 2023-03-30 02:22:02 +02:00
phillip 2a8040642a Merge branch 'legacy' into feature/legacy/mariadb_database 2023-03-28 01:26:24 +02:00
phillip cbf7aad8ac Fix Type errors 2023-03-28 01:25:33 +02:00
phillip d5a21b82de Add GitHub Link to footer 2023-03-28 01:00:41 +02:00
phillip 551536bcb4 Update bootstrap-tables 2023-03-28 01:00:21 +02:00
phillip b8220732ee Introduce "dev environment" and dotenv
There are now two dockerfiles. One for production, one for development.
All configuration is now also handled through dotenv files,
which these dotenv files, as well as the included VSCode launch tasks
use.
2023-03-28 00:54:04 +02:00
phillip 831166f38b Intall dependencies using requirements.txt 2023-03-27 23:38:22 +02:00
phillip 41a24ad9ce readd main.py 2023-03-27 01:02:26 +02:00
Phillip Kühne 6efb234a1c Merge pull request #39 from PhoenixTwoFive/feature/legacy/mariadb_database
Feature/legacy/mariadb database
2022-10-08 19:51:56 +02:00
phillip 698c3717fd Remove outdated workflows 2022-10-08 19:50:35 +02:00
phillip 8dd90b728c MySQL Working, bug fixes. 2022-10-08 19:47:29 +02:00
phillip a22960eae9 Fix DB Connection 2022-07-15 15:54:15 +02:00
phillip bc44985fed Fix DBCONNSTRING 2022-07-15 15:35:58 +02:00
phillip 72914109c0 prune requirements 2022-07-15 15:33:42 +02:00
phillip 57ced69ddd Load DB Connection from ENV 2022-07-15 15:27:36 +02:00
phillip fc78bdc4fe Fix types 2022-07-15 15:09:48 +02:00
phillip adfd120636 Update package names for 22.04 2022-07-15 15:01:07 +02:00
phillip 67a9552fee Update ubuntu to get newer mariadb connector 2022-07-15 14:55:14 +02:00
phillip 3ae2803fcc Remove mysql and install mariadb 2022-07-15 14:51:05 +02:00
phillip 2138189dff python3 is still not default on Ubuntu? 2022-07-15 14:44:26 +02:00
phillip 9907a19066 Test 2022-07-15 14:43:16 +02:00
phillip 2a18f3dc2c Completely remove mariadb libs to hopefully fix conflicts 2022-07-15 14:36:47 +02:00
phillip 69e252e28a Add missing libraries to install 2022-07-15 14:35:52 +02:00
phillip 8151e615c0 Test 2022-07-15 14:34:07 +02:00
phillip 2a92c39450 Test 2022-07-15 14:32:51 +02:00
phillip b88aac69e6 Fix.. 2022-07-15 14:32:06 +02:00
phillip c9ad755a04 Fix? 2022-07-15 14:29:18 +02:00
phillip 3527ba4fd9 Fix dependencies 2022-07-15 14:27:39 +02:00
phillip fb9677dc88 Fix github workflow 2022-07-15 14:23:17 +02:00
Phillip Kühne 97dd80b03a Add or update the Azure App Service build and deployment workflow config 2022-07-15 14:16:06 +02:00
phillip faad60346f Intial changes for MariaDB 2022-07-15 14:01:02 +02:00
phillip 3be307c0f9 Short commit hash on heroku 2022-07-15 02:59:33 +02:00
phillip 8cc9ed3645 Add version loading 2022-07-15 02:55:18 +02:00
phillip 108d70253c TEst 2022-07-15 02:34:41 +02:00
phillip 527c9d451d Refactor for heroku? 2022-07-15 02:31:58 +02:00
phillip 5beb752edf forgot gunicorn 2022-07-15 02:24:35 +02:00
phillip 589e50120b Refactor for heroku... 2022-07-15 02:22:36 +02:00
phillip f421423636 Refactor for heroku 2022-07-15 02:13:41 +02:00
phillip f6901a6175 Change 2022-07-15 02:11:12 +02:00
phillip 895dd14073 Refactor for heroku 2022-07-15 02:07:40 +02:00
phillip 95838885e7 Fix parsing of songlist download page for CSV file link 2022-04-07 21:32:11 +02:00
phillip 99c7917573 Merge branch 'legacy_feature_transferred_tagging' into legacy 2021-10-09 19:37:53 +02:00
phillip 23e7bf71bc Add transfer status toggle button 2021-10-09 19:36:28 +02:00
phillip 360b61fe21 Better version identification 2021-10-09 18:16:55 +02:00
phillip bf1b711abe Fix webmanifest 2021-10-09 01:04:23 +02:00
phillip 65635f57a8 Fix overly agressive caching 2021-10-09 01:04:15 +02:00
phillip 013dcecba9 fix launch configs 2021-10-09 00:03:56 +02:00
phillip d79eebc949 Add favicon, fix caching 2021-10-09 00:03:46 +02:00
phillip c203317320 Add Version identification to footer 2021-10-08 23:21:41 +02:00
phillip 4b70b70a1d Merge branch 'legacy_feature_client_identification_queue_limit' into stable 2021-10-08 22:44:32 +02:00
phillip 69f84332e6 style fixes 2021-10-06 02:16:24 +02:00
phillip 6a7b07ac9f data type error in config variables 2021-10-06 02:02:23 +02:00
phillip ba4b5fb57c Bessere Formulierung von Fehlern 2021-10-06 02:00:09 +02:00
phillip e6bc974a6f Eintragungstoggle fix 2021-10-06 01:59:51 +02:00
phillip 569daf0a04 Einstellungsseite hinzugefügt 2021-10-06 01:59:30 +02:00
phillip 78a39b8d17 Add client ids and quota as well as queue limit 2021-10-06 00:45:57 +02:00
phillip 14ce3e07f5 Force no scaling 2021-10-05 22:49:00 +02:00
phillip 8f769ac7e3 Configure HTTP->HTTPS redirect 2021-08-20 16:58:35 +02:00
phillip 4b2be56b3f Cosmetics 2021-08-20 15:58:45 +02:00
phillip 87a632a661 Fix breakage by not pinning versions 2021-08-20 15:47:30 +02:00
phillip 9ab1e9b841 Adapt for Google Cloud Deployment 2021-08-20 15:47:03 +02:00
phillip 080648d587 Add node_modules to .gitignore 2020-08-14 12:46:14 +02:00
phillip 986d898673 Properly pass user input to SQLite and fix searches with umlauts.
Resolves #3.
2020-03-10 20:08:31 +01:00
phillip b4418a05b6 Update vscode launch definitins 2020-03-10 18:52:56 +01:00
phillip 3ccd64bfbc Remove frontend stub from this branch 2020-03-10 18:52:45 +01:00
phillip 824184ea42 Fix CSS
Resolves #2
2020-03-06 19:37:51 +01:00
phillip 7325e901f2 Remove ugly scrolbars wen logged in 2020-03-06 19:37:32 +01:00
phillip d26c877a52 Fix song database update
Resolves #1
2020-03-06 19:37:00 +01:00
113 changed files with 2808 additions and 18568 deletions
+40
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
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
+12
View File
@@ -0,0 +1,12 @@
# MariaDB
MARIADB_ROOT_PASSWORD=mariadb_root_password
MARIADB_ROOT_HOST=localhost
MARIADB_DATABASE=karaoqueue
MARIADB_USER=karaoqueue
MARIADB_PASSWORD=mariadb_karaoqueue_password
# Karaoqueue
DEPLOYMENT_PLATFORM=Docker
DBSTRING="mysql+pymysql://karaoqueue:mariadb_karaoqueue_password@127.0.0.1:3306/karaoqueue?charset=utf8mb4"
INITIAL_USERNAME=admin
INITIAL_PASSWORD=changeme
+3
View File
@@ -0,0 +1,3 @@
[flake8]
ignore = E501
max-line-length = 120
+23
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 }}
+11 -1
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
@@ -131,3 +131,13 @@ dmypy.json
# Test data
data/
# Node Modules
node_modules/
# Version identification file
.version
# Docker secrets
secrets.yml
+52 -12
View File
@@ -4,43 +4,83 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{"name":"Python: Flask","type":"python","request":"launch","module":"flask","env":{"FLASK_APP":"backend/app/main.py","FLASK_ENV":"development","FLASK_DEBUG":"1"},"args":["run","--no-debugger","--no-reload"],"jinja":true},
{"name":"Python: Flask (with reload)","type":"python","request":"launch","module":"flask","env":{"FLASK_APP":"backend/app/main.py","FLASK_ENV":"development","FLASK_DEBUG":"1"},"args":["run","--no-debugger"],"jinja":true},
{
"name": "Python: Flask (with reload, externally reachable)",
"preLaunchTask": "mariadb",
"name": "Python: Flask",
"type": "python",
"cwd": "${workspaceFolder}/backend",
"request": "launch",
"module": "flask",
"env": {
"FLASK_APP": "backend/app/main.py",
"FLASK_APP": "backend/app.py",
"FLASK_ENV": "development",
"FLASK_DEBUG": "1"
"FLASK_DEBUG": "1",
},
"envFile": "${workspaceFolder}/.env.dev",
"args": [
"run",
"--no-debugger",
"--host='0.0.0.0'"
"--no-reload"
],
"jinja": true
},
{
"name": "Python: Flask (externally reachable)",
"preLaunchTask": "mariadb",
"name": "Python: Flask (with reload)",
"type": "python",
"cwd": "${workspaceFolder}/backend",
"request": "launch",
"module": "flask",
"env": {
"FLASK_APP": "backend/app/main.py",
"FLASK_APP": "backend/app.py",
"FLASK_ENV": "development",
"FLASK_DEBUG": "1"
},
"envFile": "${workspaceFolder}/.env.dev",
"args": [
"run",
"--no-debugger"
],
"jinja": true
},
{
"preLaunchTask": "mariadb",
"name": "Python: Flask (with reload, externally reachable)",
"type": "python",
"cwd": "${workspaceFolder}/backend",
"request": "launch",
"module": "flask",
"env": {
"FLASK_APP": "backend/app.py",
"FLASK_ENV": "development",
"FLASK_DEBUG": "1"
},
"envFile": "${workspaceFolder}/.env.dev",
"args": [
"run",
"--no-debugger",
"--host=0.0.0.0"
],
"jinja": true
},
{
"preLaunchTask": "mariadb",
"name": "Python: Flask (externally reachable)",
"type": "python",
"cwd": "${workspaceFolder}/backend",
"request": "launch",
"module": "flask",
"env": {
"FLASK_APP": "backend/app.py",
"FLASK_ENV": "development",
"FLASK_DEBUG": "1"
},
"envFile": "${workspaceFolder}/.env.dev",
"args": [
"run",
"--no-debugger",
"--no-reload",
"--host='0.0.0.0'"
"--host=0.0.0.0"
],
"jinja": true
},
+11 -6
View File
@@ -1,9 +1,14 @@
{
"python.pythonPath": "/usr/bin/python",
"files.exclude": {
"**/.classpath": true,
"**/.project": true,
"**/.settings": true,
"**/.factorypath": true
"python.testing.unittestArgs": [
"-v",
"-s",
"./backend/tests",
"-p",
"*test.py"
],
"python.testing.pytestEnabled": false,
"python.testing.unittestEnabled": true,
"emmet.includeLanguages": {
"django-html": "html"
}
}
+11 -6
View File
@@ -4,12 +4,17 @@
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"path": "frontend/ng-karaoqueue/",
"problemMatcher": [
"$tsc"
]
"label": "versiondump",
"type": "shell",
"command": "echo \"$(git rev-parse --abbrev-ref HEAD)-$(git describe)\"> ${workspaceFolder}/backend/.version",
"problemMatcher": []
},
{
"label": "mariadb",
"type": "shell",
"command": "docker-compose -f docker-compose.yml up --remove-orphans",
"isBackground": true,
"activeOnStart": false
}
]
}
+23
View File
@@ -0,0 +1,23 @@
FROM tiangolo/meinheld-gunicorn-flask:python3.9
# 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
-13
View File
@@ -1,13 +0,0 @@
{
"folders": [
{
"path": "./frontend/ng-karaoqueue"
},
{
"path": "backend"
},
{
"path": "docs"
}
]
}
+19
View File
@@ -0,0 +1,19 @@
# This file specifies files that are *not* uploaded to Google Cloud Platform
# using gcloud. It follows the same syntax as .gitignore, with the addition of
# "#!include" directives (which insert the entries of the given .gitignore-style
# file at that point).
#
# For more information, run:
# $ gcloud topic gcloudignore
#
.gcloudignore
# If you would like to upload your .git directory, .gitignore file or files
# from your .gitignore file, remove the corresponding line
# below:
.git
.gitignore
# Python pycache:
__pycache__/
# Ignored by the build system
/setup.cfg
-24
View File
@@ -1,24 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch via NPM",
"cwd": "${workspaceFolder}/karaoqueue-backend",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run-script",
"debug"
],
"port": 9229,
"skipFiles": [
"<node_internals>/**"
],
"preLaunchTask": "npm: build - karaoqueue-backend"
}
]
}
-3
View File
@@ -1,3 +0,0 @@
{
"python.pythonPath": "pyenv/bin/python"
}
-59
View File
@@ -1,59 +0,0 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "Launch MongoDB",
"type": "shell",
"command": "docker-compose -f ${workspaceFolder}/docker/docker-compose.dev.yml up",
"isBackground": true,
"problemMatcher": [
{
"pattern": [
{
"regexp": ".",
"file": 1,
"location": 2,
"message": 3
}
],
"background": {
"activeOnStart": true,
"beginsPattern": ".",
"endsPattern": "."
}
}
],
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": true
},
"group": "build"
},
{
"label": "Stop MongoDB",
"type": "shell",
"command": "docker-compose -f ${workspaceFolder}/docker/docker-compose.dev.yml stop"
},
{
"label": "Reset MongoDB",
"type": "shell",
"command": "docker-compose -f ${workspaceFolder}/docker/docker-compose.dev.yml rm -sf",
"problemMatcher": []
},
{
"type": "npm",
"script": "build",
"path": "karaoqueue-backend/",
"group": "build",
"problemMatcher": [],
"label": "npm: build - karaoqueue-backend",
"detail": "tsc"
}
]
}
+48
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"
+390
View File
@@ -0,0 +1,390 @@
from flask import Flask, render_template, abort, request, redirect, send_from_directory, jsonify
from flask.wrappers import Response
import helpers
import database
import data_adapters
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)
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(), debug=app.config['DEBUG'])
else:
return render_template('main.html', list=database.get_list(), auth=basic_auth.authenticate(), debug=app.config['DEBUG'])
@app.route("/favicon.ico")
def favicon():
return send_from_directory(os.path.join(app.root_path, 'static'),
'favicon.ico', mimetype='image/vnd.microsoft.icon')
@app.route('/api/enqueue', methods=['POST'])
@nocache
def enqueue():
if not request.json:
abort(400)
client_id = request.json['client_id']
if not helpers.is_valid_uuid(client_id):
abort(400)
name = request.json['name'].strip()
song_id = request.json['id']
if request.authorization:
entry_id = database.add_entry(name, song_id, client_id)
return Response(f"""{{"status":"OK", "entry_id":{entry_id}}}""", mimetype='text/json')
else:
if helpers.get_accept_entries(app):
if not request.json:
abort(400)
client_id = request.json['client_id']
if not helpers.is_valid_uuid(client_id):
abort(400)
name = request.json['name']
song_id = request.json['id']
if database.check_queue_length() < int(app.config['MAX_QUEUE']):
if database.check_entry_quota(client_id) < int(app.config['ENTRY_QUOTA']):
entry_id = database.add_entry(name, song_id, client_id)
return Response(f"""{{"status":"OK", "entry_id":{entry_id}}}""", mimetype='text/json')
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)
else:
return Response('{"status":"Die Warteschlange enthält momentan ' + str(database.check_queue_length()) + ' Einträge und ist lang genug, bitte versuche es noch einmal wenn ein paar Songs gesungen wurden."}', mimetype='text/json', status=423)
else:
return Response('{"status":"Currently not accepting entries"}', mimetype='text/json', status=423)
@app.route("/list")
def songlist():
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(), debug=app.config['DEBUG'])
@app.route("/settings", methods=['POST'])
@nocache
@basic_auth.required
def settings_post():
entryquota = request.form.get("entryquota")
maxqueue = request.form.get("maxqueue")
theme = request.form.get("theme")
username = request.form.get("username")
password = request.form.get("password")
changed_credentials = False
if entryquota.isnumeric() and int(entryquota) > 0: # type: ignore
app.config['ENTRY_QUOTA'] = int(entryquota) # type: ignore
else:
abort(400)
if maxqueue.isnumeric and int(maxqueue) > 0: # type: ignore
app.config['MAX_QUEUE'] = int(maxqueue) # type: ignore
else:
abort(400)
if theme is not None and theme in helpers.get_themes():
helpers.set_theme(app, theme)
else:
abort(400)
if username != "" and username != app.config['BASIC_AUTH_USERNAME']:
app.config['BASIC_AUTH_USERNAME'] = username
changed_credentials = True
if password != "":
app.config['BASIC_AUTH_PASSWORD'] = password
changed_credentials = True
helpers.persist_config(app=app)
if changed_credentials:
return redirect("/")
else:
return render_template('settings.html', app=app, auth=basic_auth.authenticate(), themes=helpers.get_themes(), debug=app.config['DEBUG'])
@app.route("/api/queue")
@nocache
def queue_json():
list = data_adapters.dict_from_rows(database.get_list())
return Response(json.dumps(list, ensure_ascii=False).encode('utf-8'), mimetype='text/json')
@app.route("/plays")
@nocache
@basic_auth.required
def played_list():
return render_template('played_list.html', list=database.get_played_list(), auth=basic_auth.authenticate(), debug=app.config['DEBUG'])
@app.route("/api/songs")
@nocache
def songs():
list = database.get_song_list()
return Response(json.dumps(list, ensure_ascii=False).encode('utf-8'), mimetype='text/json')
@app.route("/api/songs/update")
@nocache
@basic_auth.required
def update_songs():
database.delete_all_entries()
helpers.reset_current_event_id(app)
status = database.import_songs(
helpers.get_songs(helpers.get_catalog_url()))
print(status)
return Response('{"status": "%s" }' % status, mimetype='text/json')
@app.route("/api/songs/compl") # type: ignore
@nocache
def get_song_completions(input_string=""):
input_string = request.args.get('search', input_string)
if input_string != "":
result = [list(x) for x in database.get_song_completions(input_string=input_string)]
return jsonify(result)
else:
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()
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
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:
abort(400)
client_id = request.json['client_id']
if not helpers.is_valid_uuid(client_id):
abort(400)
if database.get_raw_entry(entry_id)[3] != client_id: # type: ignore
abort(403)
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", methods=['POST'])
@nocache
@basic_auth.required
def delete_entries():
if not request.json:
abort(400)
return
updates = database.delete_entries(request.json)
if updates >= 0:
return Response('{"status": "OK", "updates": ' + str(updates) + '}', mimetype='text/json')
else:
return Response('{"status": "FAIL"}', mimetype='text/json', status=400)
@app.route("/api/entries/mark_sung/<entry_id>")
@nocache
@basic_auth.required
def mark_sung(entry_id):
if database.add_sung_song(entry_id):
return Response('{"status": "OK"}', mimetype='text/json')
else:
return Response('{"status": "FAIL"}', mimetype='text/json')
@app.route("/api/entries/mark_transferred/<entry_id>")
@nocache
@basic_auth.required
def mark_transferred(entry_id):
if database.toggle_transferred(entry_id):
return Response('{"status": "OK"}', mimetype='text/json')
else:
return Response('{"status": "FAIL"}', mimetype='text/json')
@app.route("/api/entries/accept/<value>")
@nocache
@basic_auth.required
def set_accept_entries(value):
if (value == '0' or value == '1'):
helpers.set_accept_entries(app, bool(int(value)))
return Response('{"status": "OK"}', mimetype='text/json')
else:
return Response('{"status": "FAIL"}', mimetype='text/json', status=400)
@app.route("/api/entries/accept")
@nocache
def get_accept_entries():
accept_entries = helpers.get_accept_entries(app)
return Response('{"status": "OK", "value": ' + str(int(accept_entries)) + '}', mimetype='text/json')
@app.route("/api/event/close")
@nocache
@basic_auth.required
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')
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")
@nocache
@basic_auth.required
def delete_all_entries():
if 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')
@app.route("/login")
@basic_auth.required
def admin():
return redirect("/", code=303)
@app.route("/api/events/current")
@nocache
def get_current_event():
return Response('{"status": "OK", "event": "' + helpers.get_current_event_id(app) + '"}', mimetype='text/json')
def activate_job():
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
def add_header(response):
"""
Add headers to both force latest IE rendering engine or Chrome Frame,
and also to cache the rendered page for 10 minutes.
"""
if 'Cache-Control' not in response.headers:
response.headers['Cache-Control'] = 'private, max-age=600, no-cache, must-revalidate'
return response
@app.context_processor
def inject_version():
return dict(karaoqueue_version=app.config['VERSION'])
# Perform setup here so it will be executed when the module is imported by the WSGI server.
activate_job()
+19
View File
@@ -0,0 +1,19 @@
runtime: python39
manual_scaling:
# Das ist alles bloß dumm schnell zusammengehackt...
instances: 1
handlers:
# This configures Google App Engine to serve the files in the app's static
# directory.
- url: /static
static_dir: static
# This handler routes all requests not caught above to your main app. It is
# required when static routes are defined, but can be omitted (along with
# the entire handlers section) when there are no static files defined.
- url: /.*
script: auto
secure: always
redirect_http_response_code: 301
+5
View File
@@ -0,0 +1,5 @@
def dict_from_rows(rows):
outlist = []
for row in rows:
outlist.append(dict(row._mapping))
return outlist
+417
View File
@@ -0,0 +1,417 @@
# -*- coding: utf_8 -*-
from sqlalchemy import create_engine, engine, text
import pandas
from io import StringIO
from flask import current_app
import uuid
song_table = "songs"
entry_table = "entries"
index_label = "Id"
done_table = "done_songs"
sql_engine = None
def get_db_engine() -> engine.base.Engine:
global sql_engine
if (not sql_engine):
sql_engine = create_engine(
current_app.config.get("DBCONNSTRING")) # type: ignore
return sql_engine
def import_songs(song_csv):
print("Start importing Songs...")
df = pandas.read_csv(StringIO(song_csv), sep=';')
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()
print("Imported songs ({} in Database)".format(num_songs))
return ("Imported songs ({} in Database)".format(num_songs))
def import_stats(stats_csv):
print("Start importing Stats...")
df = pandas.read_csv(stats_csv, sep=',')
if (df.columns[0] != "Id" or df.columns[1] != "Playbacks"):
return False
with get_db_engine().connect() as conn:
for index, row in df.iterrows():
stmt = text(
"INSERT INTO long_term_stats (Id,Playbacks) VALUES (:par_id,:par_playbacks) ON DUPLICATE KEY UPDATE Playbacks=:par_playbacks")
conn.execute(stmt, {"par_id": row["Id"], "par_playbacks": row["Playbacks"]})
conn.commit()
return True
def create_schema():
create_song_table()
create_entry_table()
create_done_song_table()
create_config_table()
create_long_term_stats_table()
create_list_view()
create_done_song_view()
def create_entry_table():
with get_db_engine().connect() as conn:
stmt = text(
f'CREATE TABLE IF NOT EXISTS `{entry_table}` (ID INTEGER PRIMARY KEY NOT NULL AUTO_INCREMENT, Song_Id INTEGER NOT NULL, Name VARCHAR(255), Client_Id VARCHAR(36), Transferred INTEGER DEFAULT 0)')
conn.execute(stmt)
conn.commit()
def create_done_song_table():
with get_db_engine().connect() as conn:
stmt = text(
f'CREATE TABLE IF NOT EXISTS `{done_table}` (Song_Id INTEGER PRIMARY KEY NOT NULL, Plays INTEGER)')
conn.execute(stmt)
conn.commit()
def create_song_table():
with get_db_engine().connect() as conn:
stmt = text(f"""CREATE TABLE IF NOT EXISTS `{song_table}` (
`Id` INTEGER,
`Title` TEXT,
`Artist` TEXT,
`Year` VARCHAR(4),
`Duo` BOOLEAN,
`Explicit` INTEGER,
`Date Added` TIMESTAMP,
`Styles` 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()
def create_list_view():
with get_db_engine().connect() as conn:
stmt = text("""CREATE OR REPLACE VIEW `Liste` AS
SELECT Name, Title, Artist, entries.Id AS entry_ID, songs.Id AS song_ID, entries.Transferred
FROM entries, songs
WHERE entries.Song_Id=songs.Id
ORDER BY entries.Id ASC
""")
conn.execute(stmt)
conn.commit()
def create_done_song_view():
with get_db_engine().connect() as conn:
stmt = text("""CREATE OR REPLACE VIEW `Abspielliste` AS
SELECT CONCAT(Artist," - ", Title) AS Song, Plays AS Wiedergaben
FROM songs, done_songs
WHERE done_songs.Song_Id=songs.Id""")
conn.execute(stmt)
conn.commit()
def create_config_table():
with get_db_engine().connect() as conn:
stmt = text("""CREATE TABLE IF NOT EXISTS `config` (
`Key` VARCHAR(50) NOT NULL PRIMARY KEY,
`Value` TEXT
)""")
conn.execute(stmt)
conn.commit()
def get_list():
with get_db_engine().connect() as conn:
stmt = text("SELECT * FROM Liste")
cur = conn.execute(stmt)
return cur.fetchall()
def get_played_list():
with get_db_engine().connect() as conn:
stmt = text("SELECT * FROM Abspielliste")
cur = conn.execute(stmt)
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;")
cur = conn.execute(stmt)
return cur.fetchall()
def get_song_completions(input_string):
with get_db_engine().connect() as conn:
prepared_string = f"{input_string}"
prepared_string_with_wildcard = f"%{input_string}%"
stmt = text(
"""
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, "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()
def add_entry(name, song_id, client_id):
with get_db_engine().connect() as conn:
stmt = text(
"INSERT INTO entries (Song_Id,Name,Client_Id) VALUES (:par_song_id,:par_name,:par_client_id) RETURNING entries.ID;")
cur = conn.execute(stmt, {"par_song_id": song_id, "par_name": name,
"par_client_id": client_id}) # type: ignore
conn.commit()
return cur.fetchone()[0] # type: ignore
def add_sung_song(entry_id):
with get_db_engine().connect() as conn:
stmt = text("SELECT Song_Id FROM entries WHERE Id=:par_entry_id")
cur = conn.execute(stmt, {"par_entry_id": entry_id}) # type: ignore
song_id = cur.fetchone()[0] # type: ignore
stmt = text(
"INSERT INTO done_songs (Song_Id,Plays) VALUES (:par_song_id,1) ON DUPLICATE KEY UPDATE Plays=Plays + 1;")
conn.execute(stmt, {"par_song_id": song_id}) # type: ignore
conn.commit()
delete_entry(entry_id)
return True
def toggle_transferred(entry_id):
with get_db_engine().connect() as conn:
cur = conn.execute(text("SELECT Transferred FROM entries WHERE ID = :par_entry_id"),
{"par_entry_id": entry_id}) # type: ignore
marked = cur.fetchall()[0][0]
if (marked == 0):
conn.execute(text("UPDATE entries SET Transferred = 1 WHERE ID = :par_entry_id"),
{"par_entry_id": entry_id}) # type: ignore
else:
conn.execute(text("UPDATE entries SET Transferred = 0 WHERE ID = :par_entry_id"),
{"par_entry_id": entry_id}) # type: ignore
conn.commit()
return True
def check_entry_quota(client_id):
with get_db_engine().connect() as conn:
cur = conn.execute(text("SELECT Count(*) FROM entries WHERE entries.Client_Id = :par_client_id"),
{"par_client_id": client_id}) # type: ignore
return cur.fetchall()[0][0]
def check_queue_length():
with get_db_engine().connect() as conn:
cur = conn.execute(text("SELECT Count(*) FROM entries"))
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);
""")
conn.execute(stmt)
conn.commit()
return True
def clear_played_songs():
with get_db_engine().connect() as conn:
conn.execute(text("DELETE FROM done_songs"))
conn.commit()
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):
with get_db_engine().connect() as conn:
conn.execute(text("DELETE FROM entries WHERE id= :par_id"), {
"par_id": id}) # type: ignore
conn.commit()
return True
def delete_entries(ids):
idlist = []
for x in ids:
idlist.append((x,))
try:
with get_db_engine().connect() as conn:
cur = conn.execute(text("DELETE FROM entries WHERE id= :par_id"), {
"par_id": idlist})
conn.commit()
return cur.rowcount
except Exception:
return -1
def delete_all_entries() -> bool:
with get_db_engine().connect() as conn:
conn.execute(text("DELETE FROM entries"))
conn.commit()
return True
def get_config(key: str) -> str:
try:
with get_db_engine().connect() as conn:
cur = conn.execute(
text("SELECT `Value` FROM config WHERE `Key`= :par_key"), {"par_key": key}) # type: ignore
conn.commit()
return cur.fetchall()[0][0]
except IndexError:
return ""
def set_config(key: str, value: str) -> bool:
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"),
{"par_key": key, "par_value": value}
) # type: ignore
conn.commit()
return True
def get_config_list() -> dict:
with get_db_engine().connect() as conn:
cur = conn.execute(text("SELECT * FROM config"))
result_dict = {}
for row in cur.fetchall():
result_dict[row[0]] = row[1]
return result_dict
def check_config_table() -> bool:
with get_db_engine().connect() as conn:
if conn.dialect.has_table(conn, 'config'):
# type: ignore
# type: ignore
if (conn.execute(text("SELECT COUNT(*) FROM config")).fetchone()[0] > 0): # type: ignore
return True
else:
return False
else:
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")
-11
View File
@@ -1,11 +0,0 @@
FROM tiangolo/uwsgi-nginx-flask:python3.7
RUN pip install requests
RUN pip install pandas
RUN pip install Flask-BasicAuth
RUN pip install bs4
COPY ../app /app
-14
View File
@@ -1,14 +0,0 @@
version: '2'
services:
mongo:
extends:
file: docker-compose.yml
service: mongo
mongo-express:
depends_on:
- mongo
image: mongo-express
restart: always
ports:
- "8081:8081"
-12
View File
@@ -1,12 +0,0 @@
version: '2'
services:
mongo:
image: mongo
restart: always
ports:
- "27017:27017"
backend:
depends_on:
- mongo
build: .
+175
View File
@@ -0,0 +1,175 @@
import requests
from bs4 import BeautifulSoup
import subprocess
import os
import uuid
from flask import make_response, Flask
from functools import wraps, update_wrapper
from datetime import datetime
import database
def get_catalog_url():
r = requests.get('https://www.karafun.de/karaoke-song-list.html')
soup = BeautifulSoup(r.content, 'html.parser')
url = soup.findAll(
'a', href=True, text='Verfügbar in CSV-Format')[0]['href']
return url
def get_songs(url):
r = requests.get(url)
return r.text
def is_valid_uuid(val):
try:
uuid.UUID(str(val))
return True
except ValueError:
return False
def check_config_exists():
return database.check_config_table()
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") # type: ignore # noqa: E501
return
else:
app.config['VERSION'] = "Unknown"
def load_dbconfig(app: Flask):
if os.environ.get("FLASK_ENV") == "development":
app.config['DBCONNSTRING'] = os.environ.get("DBSTRING")
else:
if os.environ.get("DEPLOYMENT_PLATFORM") == "Heroku":
if os.environ.get("JAWSDB_MARIA_URL"):
app.config['DBCONNSTRING'] = os.environ.get("JAWSDB_MARIA_URL")
else:
app.config['DBCONNSTRING'] = ""
if os.environ.get("DEPLOYMENT_PLATFORM") == "Docker":
if os.environ.get("DBSTRING"):
app.config['DBCONNSTRING'] = os.environ.get("DBSTRING")
else:
app.config['DBCONNSTRING'] = ""
elif os.path.isfile(".dbconn"):
with open('.dbconn', 'r') as file:
data = file.read().replace('\n', '')
if data:
app.config['DBCONNSTRING'] = data
else:
app.config['DBCONNSTRING'] = ""
else:
exit("""No database connection string found. Cannot continue.
Please set the environment variable DBSTRING or
create a file .dbconn in the root directory of the project.""")
# Check if config exists in DB, if not, create it.
def setup_config(app: Flask):
if check_config_exists() is False:
print("No config found, creating new config")
initial_username = os.environ.get("INITIAL_USERNAME")
initial_password = os.environ.get("INITIAL_PASSWORD")
if initial_username is None:
print(
"No initial username set. Please set the environment variable INITIAL_USERNAME")
exit()
if initial_password is None:
print(
"No initial password set. Please set the environment variable INITIAL_PASSWORD")
exit()
default_config = {'username': initial_username,
'password': initial_password,
'entryquota': 2,
'maxqueue': 10,
'entries_allowed': 1,
'theme': 'default.css'}
for key, value in default_config.items():
database.set_config(key, value)
print("Created new config")
database.init_event_id()
config = database.get_config_list()
app.config['BASIC_AUTH_USERNAME'] = config['username']
app.config['BASIC_AUTH_PASSWORD'] = config['password']
app.config['ENTRY_QUOTA'] = config['entryquota']
app.config['MAX_QUEUE'] = config['maxqueue']
app.config['ENTRIES_ALLOWED'] = bool(config['entries_allowed'])
app.config['THEME'] = config['theme']
app.config['EVENT_ID'] = database.get_event_id()
# set queue admittance
def set_accept_entries(app: Flask, allowed: bool):
if allowed:
app.config['ENTRIES_ALLOWED'] = True
database.set_config('entries_allowed', '1')
else:
app.config['ENTRIES_ALLOWED'] = False
database.set_config('entries_allowed', '0')
# get queue admittance
def get_accept_entries(app: Flask) -> bool:
state = bool(int(database.get_config('entries_allowed')))
app.config['ENTRIES_ALLOWED'] = state
return state
# Write settings from current app.config to DB
def persist_config(app: Flask):
config = {'username': app.config['BASIC_AUTH_USERNAME'], 'password': app.config['BASIC_AUTH_PASSWORD'],
'entryquota': app.config['ENTRY_QUOTA'], 'maxqueue': app.config['MAX_QUEUE']}
for key, value in config.items():
database.set_config(key, value)
# Get available themes from themes directory
def get_themes():
themes = []
for theme in os.listdir('./static/css/themes'):
themes.append(theme)
return themes
# Set theme
def set_theme(app: Flask, theme: str):
if theme in get_themes():
app.config['THEME'] = theme
database.set_config('theme', theme)
else:
print("Theme not found, not setting theme.")
def 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):
@wraps(view)
def no_cache(*args, **kwargs):
response = make_response(view(*args, **kwargs))
response.headers['Last-Modified'] = datetime.now() # type: ignore
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '-1'
return response
return update_wrapper(no_cache, view)
-10
View File
@@ -1,10 +0,0 @@
# Fixed login Username
KQUEUE_USERNAME=admin
# Fixed login Password
KQUEUE_PASSWORD=pass
# Port the app is listening on
KQUEUE_PORT=3000
# Secret used to sign JSON Web Tokens
KQUEUE_JWTSECRET=THIS_IS_A_BAD_SECRET_PLEASE_CHANGE
# Expiry time for the login jwt tokens in minutes
KQUEUE_JWTEXPIRY=1440 # 24h
-117
View File
@@ -1,117 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
File diff suppressed because it is too large Load Diff
-38
View File
@@ -1,38 +0,0 @@
{
"name": "karaoqueue-backend",
"version": "0.0.1",
"description": "Backend for KaraoQueue",
"main": "index.js",
"scripts": {
"start": "node dist/index.js",
"debug": "node --nolazy dist/index.js",
"build": "tsc",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Phillip Kühne",
"license": "ISC",
"devDependencies": {
"@types/body-parser": "^1.19.0",
"@types/debug": "^4.1.5",
"@types/express": "^4.17.6",
"@types/multer": "^1.4.3",
"@types/node": "^14.0.5",
"tslint": "^6.1.2",
"typescript": "^3.9.3"
},
"dependencies": {
"@types/mongodb": "^3.5.18",
"body-parser": "^1.19.0",
"class-transformer": "^0.3.1",
"class-validator": "^0.12.2",
"cors": "^2.8.5",
"debug": "^4.1.1",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"jsonwebtoken": "^8.5.1",
"mongodb": "^3.5.7",
"multer": "^1.4.2",
"reflect-metadata": "^0.1.13",
"routing-controllers": "^0.9.0-alpha.1"
}
}
@@ -1,14 +0,0 @@
import AppState from "../models/appState.model";
import fs from "fs";
const appState = new AppState();
if (fs.existsSync("/tmp/.kqueue_eventlock")) {
appState.currentlyInEvent=true;
} else {
appState.currentlyInEvent=false;
}
appState.registrationEnabled=false;
export default appState;
@@ -1,34 +0,0 @@
import { Request, Response } from "express";
import { Post, BodyParam, Body, Res, Req, JsonController, UseBefore, Get, CookieParam } from "routing-controllers";
import User from "../interfaces/user.interface";
import { JwtMiddleware } from "../middleware/jwt.middleware";
@JsonController("/auth")
export class AuthenticationController {
@Post("/login")
doLogin(@Body() user: User, @Res() res: Response) {
if (user.username === process.env.KQUEUE_USERNAME) {
if (user.password === process.env.KQUEUE_PASSWORD) {
const jwtMiddleware = new JwtMiddleware();
const tokenData = jwtMiddleware.createToken(user);
res.cookie("jwt",tokenData,);
res.status(200);
res.send("Welcome.")
return res;
} else {
res.status(401).send("Wrong user or password.");
return res;
}
} else {
res.status(401).send("Wrong user or password.");
return res;
}
}
/* TODO Logout with JWT? */
@Get("/logout")
doLogout() {
return "//TODO logout";
}
}
@@ -1,76 +0,0 @@
import { Request, Response } from "express";
import { Controller, Get, Res, Post, Delete, Patch, Req, Authorized, CurrentUser } from "routing-controllers";
import appState from "../containers/appState.container";
import DataStoredInToken from "../interfaces/dataStoredInToken.interface";
@Controller("/queue")
export class QueueController {
/*
* Fetch entry Queue content
*/
@Get()
getQueue(@Req() req: Request, @Res() res: Response) {
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify({ placeholder: "//TODO fetch" }));
return res;
}
/*
* Add entry to Queue
*/
@Post()
addEntry(@Req() req: Request, @Res() res: Response) {
res.setHeader('Content-Type', 'application/json');
if (appState.registrationEnabled) {
res.send(JSON.stringify({ placeholder: "//TODO add" }));
} else {
res.status(403)
res.send("Entry submission is currently not allowed.")
}
return res;
}
/*
*
*/
@Delete()
@Authorized()
clearQueue(@Req() req: Request, @Res() res: Response) {
return "//TODO clear";
}
/*
*
*/
@Get("/:entry:id")
getEntry(@Req() req: Request, @Res() res: Response) {
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify({ placeholder: "//TODO get" }));
return res;
}
/*
*
*/
@Patch("/:entry_id")
editEntry(@Req() req: Request, @Res() res: Response, @CurrentUser() user?: DataStoredInToken) {
if (user) {
return "You're "+user._id;
} else {
/*TODO: Require song-specific auth to modify queue entry */
return "You are not logged in."
}
}
@Delete("/:entry_id")
deleteEntry(@Req() req: Request, @Res() res: Response, @CurrentUser() user?: DataStoredInToken) {
if (user) {
return "You're " + user._id;
} else {
/*TODO: Require song-specific auth to delete queue entry */
return "You are not logged in."
}
}
}
@@ -1,67 +0,0 @@
import { Response } from "express";
import { Authorized, Controller, Get, Param, QueryParam, Res } from "routing-controllers";
import appState from "../containers/appState.container";
import fs from "fs";
@Controller("/rpc")
@Authorized()
export class RpcController {
@Get("/start_event")
doStartEvent() {
/* TODO: Wipe state (queue and song playbacks) when starting new event. Maybe also refresh Song database automatically? */
appState.currentlyInEvent = true;
fs.openSync("/tmp/.kqueue_eventlock", 'w')
return 200;
}
@Get("/end_event")
doEndEvent() {
appState.currentlyInEvent = false;
appState.registrationEnabled = false;
fs.unlinkSync("/tmp/.kqueue_eventlock");
return 200;
}
@Get("/enable_registration")
doEnableRegistration(@Res() res: Response) {
if (appState.currentlyInEvent) {
appState.registrationEnabled = true;
return 200;
} else {
res.status(403).send("No event currently active")
return res;
}
}
@Get("/disable_registration")
doDisableRegistration(@Res() res: Response) {
if (appState.currentlyInEvent) {
appState.registrationEnabled = false;
return 200;
} else {
res.status(403).send("No event currently active")
return res;
}
}
@Get("/get_state")
doGetState() {
return appState;
}
@Get("/get_playstats")
doGetPlaystats() {
return "//TODO get_playstats"
}
@Get("/download_playstats")
doDownloadPlaystats() {
return "//TODO download_playstats"
}
@Get("/entry_fulfilled")
doEntryFulfilled(@QueryParam("entry_id") entryId: string) {
return `//TODO entry_fulfilled. entry_id: ${entryId}`
}
}
@@ -1,16 +0,0 @@
import { Get, QueryParam, JsonController, Put, Authorized } from "routing-controllers";
@JsonController("/songs")
export class SongController {
@Get()
searchSongs(@QueryParam("query") query: string, @QueryParam("limit") limit: number) {
return {result: "//TODO search"}
}
@Put()
@Authorized()
updateSongs() {
return "//TODO update"
}
}
@@ -1,9 +0,0 @@
import { JsonController, Get } from "routing-controllers";
@JsonController()
export class StatisticsController {
@Get()
getStatistics() {
return "//TODO statistics"
}
}
@@ -1,11 +0,0 @@
class HttpException extends Error {
public status: number;
public message: string;
constructor(status: number, message: string) {
super(message);
this.status = status;
this.message = message;
}
}
export default HttpException;
-74
View File
@@ -1,74 +0,0 @@
import "reflect-metadata";
import { Request, Response, Application } from "express";
import { Action, createExpressServer } from "routing-controllers";
import { QueueController } from "./controllers/queue.controller";
import { SongController } from "./controllers/songs.controller";
import { StatisticsController } from "./controllers/statistics.controller";
import { AuthenticationController } from "./controllers/auth.controller";
import { RpcController } from "./controllers/rpc.controller";
import jwt from "jsonwebtoken";
import appState from "./containers/appState.container";
import * as dotenv from "dotenv";
import DataStoredInToken from "./interfaces/dataStoredInToken.interface";
dotenv.config();
const app: Application = createExpressServer({
routePrefix: "/api",
cors: true,
/* HACK. This definitely needs to be cleaned up... */
authorizationChecker: async (action: Action) => {
const req: Request = action.request;
const secret = process.env.KQUEUE_JWTSECRET;
// tslint:disable-next-line: no-string-literal
const token = parseCookies(req.headers.cookie)['jwt'];
if (token) {
try {
const verificationResponse = jwt.verify(token, secret);
if (verificationResponse) {
return true;
} else {
return false;
}
} catch (error) {
return false;
}
} else {
return false;
}
},
/* HACK. This definitely needs to be cleaned up... */
currentUserChecker: async (action: Action) => {
const req: Request = action.request;
const secret = process.env.KQUEUE_JWTSECRET;
// tslint:disable-next-line: no-string-literal
const token = parseCookies(req.headers.cookie)['jwt'];
if (token) {
try {
const verificationResponse = jwt.verify(token, secret);
if (verificationResponse) {
return verificationResponse as DataStoredInToken;
} else {
return false;
}
} catch (error) {
return false;
}
} else {
return false;
}
},
controllers: [QueueController, SongController, StatisticsController, AuthenticationController, RpcController]
});
app.listen(process.env.KQUEUE_PORT);
/* HACK. This definitely needs to be cleaned up... */
function parseCookies(str) {
const rx = /([^;=\s]*)=([^;]*)/g;
const obj = {};
// tslint:disable-next-line: no-conditional-assignment
for (let m; m = rx.exec(str);)
obj[m[1]] = decodeURIComponent(m[2]);
return obj;
}
@@ -1,5 +0,0 @@
interface DataStoredInToken {
_id: string;
}
export default DataStoredInToken;
@@ -1,6 +0,0 @@
interface User {
username: string;
password: string;
}
export default User;
@@ -1,15 +0,0 @@
import DataStoredInToken from "../interfaces/dataStoredInToken.interface";
import User from "../interfaces/user.interface";
import * as jwt from 'jsonwebtoken';
export class JwtMiddleware {
public createToken(user: User): string {
/* expiresIn is in seconds. We take the env value which is in minutes and multiply it by 60.*/
const expiresIn = parseInt(process.env.KQUEUE_JWTEXPIRY,10) * 60;
const secret = process.env.KQUEUE_JWTSECRET;
const dataStoredInToken: DataStoredInToken = {
_id: user.username,
};
return jwt.sign(dataStoredInToken, secret, { expiresIn });
}
}
@@ -1,6 +0,0 @@
class AppState {
registrationEnabled: boolean;
currentlyInEvent: boolean;
}
export default AppState;
-17
View File
@@ -1,17 +0,0 @@
{
"compilerOptions": {
"target": "es2017",
"module": "commonjs",
"outDir": "dist",
"sourceMap": true,
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules"
]
}
-13
View File
@@ -1,13 +0,0 @@
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended"
],
"jsRules": {},
"rules": {
"trailing-comma": [
false
]
},
"rulesDirectory": []
}
+4
View File
@@ -0,0 +1,4 @@
from app import app
if __name__ == "__main__":
app.run()
+40
View File
@@ -0,0 +1,40 @@
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
+246
View File
@@ -0,0 +1,246 @@
: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 {
color: var(--text-color);
}
table td {
overflow: hidden;
text-overflow: ellipsis;
color: var(--text-color);
}
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);
--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;
--text-color-light: #6c757d;
/* 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;
/* Tables */
--table-border-color: #232323;
}
}
+91
View File
@@ -0,0 +1,91 @@
.navbar {
background: #090a28 !important;
}
.navbar .navbar-toggle:hover,
.navbar .navbar-toggle:focus {
background-color: #900000 !important;
}
.navbar .navbar-toggle {
border: none;
}
.navbar .navbar-nav>.open>a,
.navbar .navbar-nav>.open>a:hover,
.navbar .navbar-nav>.open>a:focus {
color: #CF2323 !important;
background-color: #050515 !important;
}
.btn-primary {
background-color: #15175b;
border-color: #15175b;
}
.btn-primary:hover,
.btn-primary:focus {
background-color: #0e103e;
background-position: 0 -15px;
}
.btn-primary:active,
.btn-primary.active {
background-color: #0e103e;
border-color: #0e103e;
}
.btn-primary.disabled,
.btn-primary:disabled,
.btn-primary[disabled] {
background-color: #0e103e;
background-image: none;
}
.dropdown-menu>li>a:hover,
.dropdown-menu>li>a:focus {
color: #8A0711;
}
.dropdown-menu>.active>a,
.dropdown-menu>.active>a:hover,
.dropdown-menu>.active>a:focus {
background-color: #2e6da4;
background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
background-repeat: repeat-x;
}
.navbar .navbar-nav>li>a:hover {
color: #b60000;
}
.form-control:focus {
border-color: rgba(21, 23, 91, 0.8);
outline: 0;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075),
0 0 8px rgba(21, 23, 91, 0.6);
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset,
0 0 8px rgba(21, 23, 91, 0.6);
}
a {
color: #900000;
}
a:hover,
a:focus {
color: #670000;
}
.navbar-brand {
display: flex;
align-items: center;
}
.navbar-brand > img {
height: 4rem;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+17
View File
@@ -0,0 +1,17 @@
{
"name": "KaraoQueue",
"short_name": "KaraoQueue",
"start_url": "/",
"url": "https://karaoqueue-323511.appspot.com/",
"display": "standalone",
"background_color": "#343a40",
"description": "Eine Karaokewarteliste.",
"icons": [{
"src": "images/touch/homescreen512.png",
"sizes": "512x512",
"type": "image/png"
}],
"related_applications": [{
"platform": "Web"
}]
}
+201
View File
@@ -0,0 +1,201 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<meta name="color-scheme" content="light dark" />
<link rel="icon" href="favicon.ico">
<link rel="manifest" href="/static/manifest.webmanifest">
<title>{% block title %}{% endblock %} - KaraoQueue</title>
<!-- Bootstrap-Tables -->
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.22.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://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css"
integrity="sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N" crossorigin="anonymous">
<!-- Custom styles for this template -->
<link href="static/css/style.css" rel="stylesheet">
<!-- Fontawesome Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw=="
crossorigin="anonymous" referrerpolicy="no-referrer" />
<!-- Bootstraptoggle -->
<link href="https://gitcdn.github.io/bootstrap-toggle/2.2.2/css/bootstrap-toggle.min.css" rel="stylesheet">
<!-- Active Theme -->
<link href="static/css/themes/{{config['THEME']}}" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
<a class="navbar-brand" href="/">KaraoQueue</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault"
aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarsExampleDefault">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="/">Warteliste</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/list">Songsuche</a>
</li>
{% if auth %}
<li class="nav-item">
<a class="nav-link" href="/plays">Abspielliste</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/settings">Einstellungen</a>
</li>
{% endif %}
</ul>
<!--<form class="form-inline my-2 my-lg-0">
<input class="form-control mr-sm-2" type="text" placeholder="Search" aria-label="Search">
<button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
</form>-->
</div>
</nav>
<div class="site">
<main role="main" class="container">
{% block content %}{% endblock %}
</main><!-- /.container -->
</div>
<!-- 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-24 - <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://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://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.6.2/dist/js/bootstrap.min.js"
integrity="sha384-+sLIOodYLS7CIrQpBjl+C7nPvqq+FbNUBDunl/OZv93DB7Ln/533i8e/mZXLi/P+"
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>
<script src="https://unpkg.com/bootstrap-table@1.22.2/dist/bootstrap-table.min.js"></script>
<script
src="https://unpkg.com/bootstrap-table@1.22.2/dist/extensions/auto-refresh/bootstrap-table-auto-refresh.min.js"></script>
<script src="https://gitcdn.github.io/bootstrap-toggle/2.2.2/js/bootstrap-toggle.min.js"></script>
<script src="https://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 () {
checkEventID()
// get current URL path and assign 'active' class
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() {
var dt = new Date().getTime();
var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = (dt + Math.random() * 16) % 16 | 0;
dt = Math.floor(dt / 16);
return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
return uuid;
}
function loadOrGenerateClientId() {
if (!localStorage.getItem("clientId")) {
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;
}
function dataRequestQueueAndDestroyTooltips(params) {
document.querySelectorAll(".tooltip[role='tooltip']").forEach((tooltip) => tooltip.remove());
var base_url = "/api/queue"
$.get(base_url).then((res) => {
params.success(res);
});
}
</script>
</body>
</html>
+73
View File
@@ -0,0 +1,73 @@
{% extends 'base.html' %}
{% block title %}Warteliste{% endblock %}
{% block content %}
<a id="bfb" role="button" class="btn btn-primary btn-lg btn-block mb-2" href="/list">Eintragen</a>
<table class="table entries"
data-toggle="table"
data-ajax="dataRequestQueueAndDestroyTooltips"
data-pagination="true"
data-classes="table"
data-show-refresh="false"
data-auto-refresh="true"
data-auto-refresh-interval="10">
<thead>
<tr>
<th data-field="Name">Name</th>
<th data-field="Title">Song</th>
<th data-field="Artist">Künstler</th>
<th scope="col" data-formatter="TableActionsFormatter"></th>
</tr>
</thead>
</table>
<a name="end"></a>
{% 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) {
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 %}
+220
View File
@@ -0,0 +1,220 @@
{% extends 'base.html' %}
{% block title %}Warteliste-Admin{% endblock %}
{% block content %}
<style>
table td:nth-child(2) {
overflow-y: hidden;
overflow-x: auto;
text-overflow: clip;
max-width: 200px !important;
}
</style>
<div class="container">
<div id="toolbar">
<button type="button" class="topbutton btn btn-danger" onclick="confirmDeleteSelectedEntries()"><i
class="fas fa-trash mr-2"></i>Gewählte Einträge löschen</button>
<button type="button" class="topbutton btn btn-danger" onclick="confirmUpdateSongDatabase()"><i
class="fas fa-file-import mr-2"></i>Song-Datenbank
aktualisieren</button>
<input id="entryToggle" type="checkbox" class="topbutton" data-toggle="toggle" data-on="Eintragen erlaubt"
data-off="Eintragen deaktiviert" data-onstyle="success" data-offstyle="danger">
</div>
<table class="table entries" id="entrytable" data-toggle="table" data-search="true" data-show-columns="true"
data-show-toggle="true" data-multiple-select-row="true" data-click-to-select="true" data-toolbar="#toolbar"
data-pagination="true" data-show-extended-pagination="true" data-classes="table table-hover"
data-ajax="dataRequestQueueAndDestroyTooltips" data-show-refresh="true" data-auto-refresh="true" data-auto-refresh-interval="10">
<thead>
<tr>
<th data-field="state" data-checkbox="true"></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>
</table>
<a name="end"></a>
</div>
{% endblock %}
{% block extrajs %}
<script>
$(function () {
refreshEntryToggle()
$('#entryToggle').change(function () {
$.ajax({ url: "/api/entries/accept/" + ($('#entryToggle').is(":checked") ? "1" : "0"), complete: setTimeout(refreshEntryToggle, 1000) });
})
$("#entrytable").bootstrapTable().on('load-success.bs.table', function () {
$('[data-toggle="tooltip"]').tooltip()
})
$('[data-toggle="tooltip"]').tooltip({
trigger: 'hover'
})
})
function confirmDeleteEntry(name, entry_id) {
bootbox.confirm("Wirklich den Eintrag von " + name + " löschen?", function (result) {
if (result) {
deleteEntry(entry_id)
}
})
}
function confirmDeleteSelectedEntries() {
bootbox.confirm({
message: "Wirklich gewählte Eintragungen löschen?",
buttons: {
confirm: {
label: 'Ja',
className: 'btn btn-danger'
},
cancel: {
label: 'Nein',
className: 'btn btn-secondary'
}
},
callback: function (result) {
if (result) {
DeleteSelectedEntries(getIdSelections())
}
}
})
}
function confirmUpdateSongDatabase() {
bootbox.confirm({
message: "Wirklich die Song-Datenbank aktualisieren?<br>Dies lädt die Aktuelle Song-Liste von <a href='https://www.karafun.de/karaoke-song-list.html'>KaraFun</a> herunter, <b>und wird alle Eintragungen löschen!</b>",
buttons: {
confirm: {
label: 'Ja',
className: 'btn-primary'
},
cancel: {
label: 'Nein',
className: 'btn btn-secondary'
}
},
callback: function (result) {
if (result) {
var dialog = bootbox.dialog({
message: '<p class="text-center mb-0"><i class="fa fa-spin fa-cog"></i> Aktualisiere Song-Datenbank...</p>',
closeButton: false
});
updateSongDatabase(dialog)
}
}
})
}
function refreshEntryToggle() {
$.getJSON("/api/entries/accept", (data) => {
if (data["value"] != $('#entryToggle').is(":checked")) {
if (data["value"] == 1) {
$('#entryToggle').data('bs.toggle').on('true')
}
else {
$('#entryToggle').data('bs.toggle').off('true')
}
}
})
}
function deleteEntry(entry_id) {
$.ajax({
type: 'GET',
url: '/api/entries/delete/' + entry_id,
contentType: "application/json",
dataType: 'json',
async: false
});
$("#entrytable").bootstrapTable('refresh')
}
function markEntryAsSung(entry_id) {
$.ajax({
type: 'GET',
url: '/api/entries/mark_sung/' + entry_id,
contentType: "application/json",
dataType: 'json',
async: false
});
$("#entrytable").bootstrapTable('refresh')
}
function markEntryAsTransferred(entry_id) {
$.ajax({
type: 'GET',
url: '/api/entries/mark_transferred/' + entry_id,
contentType: "application/json",
dataType: 'json',
async: false
});
$("#entrytable").bootstrapTable('refresh')
}
function DeleteSelectedEntries(ids) {
$.ajax({
type: 'POST',
url: '/api/entries/delete',
data: JSON.stringify(ids),
error: function () {
bootbox.alert({
message: "Fehler beim Löschen der Eintragungen.",
})
},
success: function () {
$("#entrytable").bootstrapTable('refresh')
},
contentType: "application/json",
dataType: 'json'
});
}
function updateSongDatabase(wait_dialog) {
$.ajax({
type: 'GET',
url: '/api/songs/update',
contentType: "application/json",
dataType: 'json',
success: function (data) {
wait_dialog.modal('hide')
bootbox.alert({
message: data["status"],
callback: function () {
$("#entrytable").bootstrapTable('refresh')
}
})
}
});
}
function TableActions(value, row, index) {
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(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(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 %}
+110
View File
@@ -0,0 +1,110 @@
{% extends 'base.html' %}
{% block title %}Abspielliste{% endblock %}
{% block content %}
<div id="toolbar">
<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
class="fas fa-print mr-2"></i>Drucken</button>
</div>
<table class="table"
id="table"
data-toggle="table"
data-search="true"
data-show-columns="true"
data-toolbar="#toolbar"
data-pagination="true"
data-classes="table table-bordered table-striped"
data-show-extended-pagination="true">
<thead>
<tr>
<th scope="col">Song</th>
<th scope="col">Wiedergaben</th>
</tr>
</thead>
{% for entry in list: %}
<tr>
<td>
{{ entry[0] }}
</td>
<td>
{{ entry[1] }}
</td>
</tr>
{% endfor %}
</table>
</table>
{% endblock %}
{% block extrajs %}
<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 confirmCloseEvent() {
bootbox.confirm({
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',
className: 'btn btn-danger'
},
cancel: {
label: 'Nein',
className: 'btn btn-secondary'
}
},
callback: function(result){
if (result) {
closeEvent()
}
}
})
}
function closeEvent() {
$.ajax({
type: 'GET',
url: '/api/event/close',
contentType: "application/json",
dataType: 'json',
async: false
});
location.reload();
}
function exportPDF() {
var doc = new jsPDF();
doc.autoTable({
head: [["Song","Wiedergaben"]],
body: createTableArray(),
theme: 'grid'
});
doc.save('Abspielliste.pdf');
}
function printPDF() {
var doc = new jsPDF();
doc.autoTable({
head: [["Song","Wiedergaben"]],
body: createTableArray(),
theme: 'grid'
});
doc.autoPrint();
doc.output('dataurlnewwindow');
}
function createTableArray() {
var data = $("#table").bootstrapTable('getData')
out = data.map(x => [x["0"],x["1"]])
return out;
}
</script>
{% endblock %}
+102
View File
@@ -0,0 +1,102 @@
{% extends 'base.html' %}
{% block title %}Einstellungen{% endblock %}
{% block content %}
<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']}}>
</p>
<p>
<label for="maxqueue">Maximale Anzahl an Einträgen Insgesamt</label>
<input type="number" class="form-control" id="maxqueue" name="maxqueue" min=1 value={{app.config['MAX_QUEUE']}}>
</p>
<p>
<label for="theme">Aktives Theme</label>
<select class="form-control" id="theme" name="theme">
{% for theme in themes %}
<option value="{{theme}}" {% if theme==config['THEME'] %}selected{% endif %}>{{theme}}</option>
{% endfor %}
</select>
</p>
<div class="alert alert-warning" role="alert">
<i class="fas fa-exclamation-triangle mr-1"></i>
<strong>Warnung:</strong> Änderungen an den folgenden Einstellungen führen zu einer sofortigen Abmeldung!
</div>
<p>
<label for="username">Benutzername</label>
<input type="text" class="form-control" id="username" name="username"
value={{app.config['BASIC_AUTH_USERNAME']}}>
</p>
<p>
<label for="password">Passwort ändern</label>
<input type="password" class="form-control" id="password" name="password">
</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 %}
+331
View File
@@ -0,0 +1,331 @@
{% extends 'base.html' %}
{% block title %}Songsuche{% endblock %}
{% block content %}
<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>
</table>
<div class="modal fade" id="enqueueModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<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">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">Schließen</button>
<button type="submit" class="btn btn-primary" id="submitSongButton">Anmelden</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extrajs %}
<script>
$(document).ready(function () {
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 = {
"name": name,
"id": id,
"client_id": client_id
}
$.ajax({
type: 'POST',
url: '/api/enqueue',
data: JSON.stringify(data),
success: success_callback,
statusCode: {
423: blocked_callback
},
contentType: "application/json",
dataType: 'json'
});
}
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) {
entryID = response["entry_id"];
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,
});
entriesAccepted();
$("#enqueueModal").modal('hide');
});
}
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) => {
if (data["value"] == 0) {
$(".enqueueButton").prop("disabled", true)
$(".enqueueButton").prop("style", "pointer-events: none;")
$(".enqueueButton").wrap("<span class='tooltip-span' tabindex='0' data-toggle='tooltip' data-placement='top'></span>");
$(".tooltip-span").prop("title", "Eintragungen sind leider momentan nicht möglich.")
$('[data-toggle="tooltip"]').tooltip()
} else {
$(".enqueueButton").prop("disabled", false)
}
})
}
{% else %}
function entriesAccepted() {
$(".enqueueButton").prop("disabled", false)
}
{% endif %}
</script>
{% endblock %}
+39
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
+23
View File
@@ -0,0 +1,23 @@
version: "3.9"
secrets:
secrets:
file: ./secrets.yml
services:
karaoqueue:
image: "ghcr.io/phoenixtwofive/karaoqueue:v2023.06"
build: .
restart: always
ports:
- "127.0.0.1:8081:80" # Please put a reverse proxy in front of this
env_file: .env
db:
image: mariadb
restart: always
env_file: .env
volumes:
- karaoqueue-db:/var/lib/mysql
volumes:
karaoqueue-db:
+15
View File
@@ -0,0 +1,15 @@
# This Compose file is for development only. It is not intended for production use.
# It only starts auxiliary services, such as a database, that are required for the
# application to run. The application itself is started separately, using the
# command "python -m flask run" or your favorite IDE.
# Useful for attaching a debugger to the application.
version: "3.9"
services:
db:
image: mariadb
restart: always
env_file: .env.dev
ports:
- "3306:3306"
-392
View File
@@ -1,392 +0,0 @@
openapi: 3.0.2
info:
title: Karaoqueue API
version: '0.0.1'
servers:
- url: 'http://localhost:3000/api'
description: Local Test sever instance
- url: 'https://karaoke.phillipathome.dynv6.net/api'
description: Production API
paths:
/queue:
get:
summary: 'Fetch entry Queue content'
description: 'Fetch entry Queue'
parameters:
- name: index
in: query
description: Position from which on to return results
required: false
schema:
type: integer
- name: limit
in: query
description: How many items to return at one time (max 100, default 20)
required: false
schema:
type: integer
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/QueueEntry'
'400':
description: Invalid request. Check your parameters.
'404':
description: No Entries found in specified range.
'5XX':
description: Unexpected error.
post:
description: 'Add entry to Queue'
summary: 'Add entry to Queue'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
singer_name:
type: string
song_id:
type: integer
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
entry_id:
type: string
format: bson.ObjectID
pattern: '/^[a-f\d]{24}$/i'
entry_auth:
type: string
'400':
description: Malformed request.
'405':
description: Currently not accepting entries.
delete:
summary: 'Clear queue'
security:
- cookieAuth: []
responses:
'200':
description: OK. Successfully cleared Queue
'401':
description: Not Authorized
description: clear queue
/queue/{entry_id}:
get:
summary: GET single queue entry
parameters:
- in: path
name: entry_id
schema:
type: integer
required: true
description: ID of the Entry to get
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/QueueEntry'
patch:
summary: Change entry
security:
- cookieAuth: []
parameters:
- in: path
name: entry_id
schema:
type: string
format: bson.ObjectID
pattern: '/^[a-f\d]{24}$/i'
required: true
description: >
ID of the entry to modify. One of the following is needed:
- Proper Bearer-Token authorization
- The entry_auth string corresponding to the entry
requestBody:
required: false
content:
application/json:
schema:
type: object
required:
- entry_auth
properties:
singer_name:
type: string
song_id:
type: integer
entry_auth:
type: string
responses:
'200':
description: OK
'404':
description: Entry not found
'405':
description: Method not allowed. Check your entry_auth or authorization.
delete:
summary: 'Delete entry'
security:
- cookieAuth: []
parameters:
- in: path
name: entry_id
schema:
type: string
format: bson.ObjectID
pattern: '/^[a-f\d]{24}$/i'
required: true
description: >
ID of the entry to modify. One of the following is needed:
- Proper Bearer-Token authorization
- The entry_auth string corresponding to the entry
requestBody:
required: false
content:
application/json:
schema:
type: object
required:
- entry_auth
properties:
entry_auth:
type: string
responses:
'200':
description: OK
'404':
description: Entry not found
'405':
description: Method not allowed. Check your entry_auth or authorization.
/songs:
get:
summary: Search in Songs
parameters:
- in: query
name: query
schema:
type: string
required: true
- name: limit
in: query
description: How many items to return at one time (max 100, default 20)
required: false
schema:
type: integer
responses:
'200':
description: OK. An array of Songs according to the Query string.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/SongEntry'
'400':
description: Malformed Request
put:
summary: Update Song Database
description: >
Trigger an update of the database using the source CSV defined
in the config.
security:
- cookieAuth: []
responses:
'200':
description: OK. Songs have been updated
'401':
description: Authorization required. Check your auth.
/statistics:
get:
summary: Statistics about the Database
responses:
'200':
description: Statistics as JSON
content:
application/json:
schema:
type: object
properties:
num_songs:
type: integer
num_entries:
type: integer
/auth/login:
post:
summary: Logs in and returns the authentication cookie
requestBody:
required: true
description: A JSON object containing the login and password.
content:
application/json:
schema:
$ref: '#/components/schemas/LoginRequest'
security: [] # no authentication
responses:
'200':
description: >
Successfully authenticated.
The session ID is returned in a cookie named `jwt`. You need to include this cookie in subsequent requests.
headers:
Set-Cookie:
schema:
type: string
example: jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiJhZG1pbiIsImlhdCI6MTYwMTY1MDYwNSwiZXhwIjoxNjAxNjU0MjA1fQ.uGvOlBAZdbPT8U9s7jEt5PUWyxLrpgaf02EoPVC_Zlsd; Path=/; HttpOnly
/auth/logout:
get:
summary: Logs the user out and invalidates the session on the server
security:
- cookieAuth: []
responses:
'200':
description: OK.
'401':
description: Authorization required.
/rpc/end_event:
get:
summary: End the current event
description: Locks entries and does not allow reopening without reset
security:
- cookieAuth: []
responses:
'200':
description: OK.
/rpc/start_event:
get:
summary: Start new event. Clears entries and stats.
description: Sets up a clean state.
security:
- cookieAuth: []
responses:
'200':
description: OK.
/rpc/enable_registration:
get:
summary: Enables registration in the queue
description: Makes it possible for guests to register in the queue.
security:
- cookieAuth: []
responses:
'200':
description: OK.
/rpc/disable_registration:
get:
summary: Disables registration in the queue
description: Makes it impossible for guests to register in the queue.
security:
- cookieAuth: []
responses:
'200':
description: OK.
/rpc/get_playstats:
get:
summary: Get stats of played songs in the current event.
description: Returns the stats for the current evening as JSON
security:
- cookieAuth: []
responses:
'200':
description: OK.
/rpc/download_playstats:
get:
summary: Get stats of played songs in the current event for download.
description: Returns the stats for the current evening as PDF (for example for GEMA)
security:
- cookieAuth: []
responses:
'200':
description: OK.
/rpc/entry_fulfilled:
get:
summary: Mark an entry as fulfilled.
description: Mark an entry as fulfilled. This adds it to the statistics.
security:
- cookieAuth: []
responses:
'200':
description: OK.
parameters:
- in: query
name: entry_id
schema:
type: string
description: The id of the entry to mark as done.
components:
securitySchemes:
cookieAuth: # arbitrary name for the security scheme; will be used in the "security" key later
type: apiKey
in: cookie
name: jwt # cookie name
schemas:
QueueEntry:
type: object
properties:
_id:
type: string
format: bson.ObjectID
pattern: '/^[a-f\d]{24}$/i'
singer_name:
type: string
song_id:
type: integer
SongEntry:
type: object
properties:
id:
type: integer
title:
type: string
artist:
type: string
year:
type: integer
duet:
type: boolean
explicit:
type: boolean
styles:
type: array
items:
type: string
languages:
type: array
items:
type: string
LoginRequest:
type: object
properties:
username:
type: string
password:
type: string
format: password
-13
View File
@@ -1,13 +0,0 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
max_line_length = off
trim_trailing_whitespace = false
-46
View File
@@ -1,46 +0,0 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out
# dependencies
/node_modules
# profiling files
chrome-profiler-events*.json
speed-measure-plugin*.json
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db
-25
View File
@@ -1,25 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "firefox",
"request": "launch",
"reAttach": true,
"name": "Launch Firefox against localhost",
"url": "http://localhost:4200",
"webRoot": "${workspaceFolder}"
},
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:4200",
"webRoot": "${workspaceFolder}",
"runtimeExecutable": "/usr/bin/chromium"
}
]
}
-26
View File
@@ -1,26 +0,0 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"group": "build",
"problemMatcher": []
},
{
"type": "npm",
"script": "build",
"group": "build",
"problemMatcher": []
},
{
"type": "npm",
"script": "serve",
"problemMatcher": [
"$tsc"
]
}
]
}
-27
View File
@@ -1,27 +0,0 @@
# NgKaraoqueue
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.3.20.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
-143
View File
@@ -1,143 +0,0 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"ng-karaoqueue": {
"projectType": "application",
"schematics": {},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/ng-karaoqueue",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"aot": false,
"assets": [
"src/favicon.ico",
"src/assets",
{
"glob": "mdi.svg",
"input": "node_modules/@mdi/angular-material/",
"output": "/assets/"
}
],
"styles": [
"src/custom-theme.scss",
"src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
]
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "ng-karaoqueue:build"
},
"configurations": {
"production": {
"browserTarget": "ng-karaoqueue:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "ng-karaoqueue:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
"src/favicon.ico",
"src/assets",
{
"glob": "mdi.svg",
"input": "node_modules/@mdi/angular-material/",
"output": "/assets/"
}
],
"styles": [
"./app/custom-theme.scss",
"src/styles.scss"
],
"scripts": []
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json"
],
"exclude": [
"**/node_modules/**"
]
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "ng-karaoqueue:serve"
},
"configurations": {
"production": {
"devServerTarget": "ng-karaoqueue:serve:production"
}
}
}
}
}
},
"defaultProject": "ng-karaoqueue",
"schematics": {
"@schematics/angular:component": {
"styleext": "scss"
}
}
}
-12
View File
@@ -1,12 +0,0 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# You can see what browsers were selected by your queries by running:
# npx browserslist
> 0.5%
last 2 versions
Firefox ESR
not dead
not IE 9-11 # For IE 9-11 support, remove 'not'.
@@ -1,32 +0,0 @@
// @ts-check
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter } = require('jasmine-spec-reporter');
/**
* @type { import("protractor").Config }
*/
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./src/**/*.e2e-spec.ts'
],
capabilities: {
browserName: 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: require('path').join(__dirname, './tsconfig.json')
});
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
}
};
@@ -1,23 +0,0 @@
import { AppPage } from './app.po';
import { browser, logging } from 'protractor';
describe('workspace-project App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should display welcome message', () => {
page.navigateTo();
expect(page.getTitleText()).toEqual('ng-karaoqueue app is running!');
});
afterEach(async () => {
// Assert that there are no errors emitted from the browser
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
expect(logs).not.toContain(jasmine.objectContaining({
level: logging.Level.SEVERE,
} as logging.Entry));
});
});
-11
View File
@@ -1,11 +0,0 @@
import { browser, by, element } from 'protractor';
export class AppPage {
navigateTo() {
return browser.get(browser.baseUrl) as Promise<any>;
}
getTitleText() {
return element(by.css('app-root .content span')).getText() as Promise<string>;
}
}
-13
View File
@@ -1,13 +0,0 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"module": "commonjs",
"target": "es5",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}
-32
View File
@@ -1,32 +0,0 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, './coverage/ng-karaoqueue'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};
File diff suppressed because it is too large Load Diff
-54
View File
@@ -1,54 +0,0 @@
{
"name": "ng-karaoqueue",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"serve": "ng serve --host=0.0.0.0",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular/animations": "~9.0.5",
"@angular/cdk": "~9.1.2",
"@angular/common": "~9.0.5",
"@angular/compiler": "~9.0.5",
"@angular/core": "~9.0.5",
"@angular/forms": "~9.0.5",
"@angular/material": "^9.1.2",
"@angular/platform-browser": "~9.0.5",
"@angular/platform-browser-dynamic": "~9.0.5",
"@angular/router": "~9.0.5",
"@mdi/angular-material": "^5.0.45",
"@ngx-pwa/local-storage": "^9.0.3",
"hammerjs": "^2.0.8",
"runtime-config-loader": "^2.0.0",
"rxjs": "~6.5.4",
"tslib": "^1.11.1",
"zone.js": "~0.10.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "^0.900.5",
"@angular/cli": "~9.0.5",
"@angular/compiler-cli": "~9.0.5",
"@angular/language-service": "~9.0.5",
"@types/jasmine": "~3.5.9",
"@types/jasminewd2": "~2.0.8",
"@types/node": "~13.9.0",
"codelyzer": "^5.2.1",
"jasmine-core": "~3.5.0",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~4.4.1",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~2.1.1",
"karma-jasmine": "~3.1.1",
"karma-jasmine-html-reporter": "^1.5.2",
"protractor": "~5.4.3",
"ts-node": "~8.6.2",
"tslint": "~6.0.0",
"typescript": "~3.8.3"
}
}
@@ -1,16 +0,0 @@
import { EntryListingComponent } from './entry-listing/entry-listing.component';
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { SongSearchComponent } from './song-search/song-search.component';
const routes: Routes = [
{path: '', component: EntryListingComponent },
{path: 'songs', component: SongSearchComponent}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
@@ -1,11 +0,0 @@
<div>
<mat-toolbar color="primary" class="mat-elevation-z6">
<span>KaraoQueue</span>
<span>&nbsp;</span>
<a mat-button routerLink="/" [routerLinkActive]="['is-active']">Warteschlange</a>
<a mat-button routerLink="/songs" [routerLinkActive]="['is-active']">Songs</a>
<div class="spacer"></div>
<a mat-stroked-button>Login</a>
</mat-toolbar>
</div>
<router-outlet></router-outlet>
@@ -1,7 +0,0 @@
.spacer {
flex: 1 1 auto;
}
.is-active {
background-color: rgba(255,255,255,0.1);
}
@@ -1,35 +0,0 @@
import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
AppComponent
],
}).compileComponents();
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'ng-karaoqueue'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('ng-karaoqueue');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('.content span').textContent).toContain('ng-karaoqueue app is running!');
});
});
@@ -1,16 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { RuntimeConfigLoaderService } from 'runtime-config-loader';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit{
constructor(private configSvc: RuntimeConfigLoaderService) {}
ngOnInit(): void {
console.log("API at ",this.configSvc.getConfigObjectKey("api"));
}
title = 'KaraoQueue';
}
@@ -1,107 +0,0 @@
import { BrowserModule, DomSanitizer } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatBadgeModule } from '@angular/material/badge';
import { MatBottomSheetModule } from '@angular/material/bottom-sheet';
import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatChipsModule } from '@angular/material/chips';
import { MatStepperModule } from '@angular/material/stepper';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatDialogModule } from '@angular/material/dialog';
import { MatDividerModule } from '@angular/material/divider';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatIconRegistry, MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatListModule } from '@angular/material/list';
import { MatMenuModule } from '@angular/material/menu';
import { MatNativeDateModule, MatRippleModule } from '@angular/material/core';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatRadioModule } from '@angular/material/radio';
import { MatSelectModule } from '@angular/material/select';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatSliderModule } from '@angular/material/slider';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatTreeModule } from '@angular/material/tree';
import { HttpClientModule } from '@angular/common/http';
import { EntryListingComponent } from './entry-listing/entry-listing.component';
import { SongSearchComponent } from './song-search/song-search.component';
import { debounceTime, distinctUntilChanged } from "rxjs/operators";
import { RuntimeConfigLoaderModule } from 'runtime-config-loader';
@NgModule({
declarations: [
AppComponent,
EntryListingComponent,
SongSearchComponent
],
imports: [
BrowserModule,
AppRoutingModule,
BrowserAnimationsModule,
MatAutocompleteModule,
MatBadgeModule,
MatBottomSheetModule,
MatButtonModule,
MatButtonToggleModule,
MatCardModule,
MatCheckboxModule,
MatChipsModule,
MatStepperModule,
MatDatepickerModule,
MatDialogModule,
MatDividerModule,
MatExpansionModule,
MatGridListModule,
MatIconModule,
MatInputModule,
MatListModule,
MatMenuModule,
MatNativeDateModule,
MatPaginatorModule,
MatProgressBarModule,
MatProgressSpinnerModule,
MatRadioModule,
MatRippleModule,
MatSelectModule,
MatSidenavModule,
MatSliderModule,
MatSlideToggleModule,
MatSnackBarModule,
MatSortModule,
MatTableModule,
MatTabsModule,
MatToolbarModule,
MatTooltipModule,
MatTreeModule,
HttpClientModule,
RuntimeConfigLoaderModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {
constructor(matIconRegistry: MatIconRegistry, domSanitizer: DomSanitizer) {
matIconRegistry.addSvgIconSet(domSanitizer.bypassSecurityTrustResourceUrl('./assets/mdi.svg'));
}
}
@@ -1 +0,0 @@
<p>entry-listing works!</p>
@@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { EntryListingComponent } from './entry-listing.component';
describe('EntryListingComponent', () => {
let component: EntryListingComponent;
let fixture: ComponentFixture<EntryListingComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ EntryListingComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(EntryListingComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,15 +0,0 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-entry-listing',
templateUrl: './entry-listing.component.html',
styleUrls: ['./entry-listing.component.scss']
})
export class EntryListingComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}
@@ -1,16 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { EntryServiceService } from './entry-service.service';
describe('EntryServiceService', () => {
let service: EntryServiceService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(EntryServiceService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -1,25 +0,0 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { RuntimeConfigLoaderService } from 'runtime-config-loader';
import { Entry } from './models/entry.model';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class EntryServiceService {
private api: string;
constructor(
private http: HttpClient,
private configSvc: RuntimeConfigLoaderService
) {
this.api = configSvc.getConfigObjectKey("api");
}
getEntries(): Observable<Array<Entry>> {
return null; // TODO
}
}
@@ -1,10 +0,0 @@
export class Artist {
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
id: number;
name: string;
}
@@ -1,12 +0,0 @@
export class Entry {
constructor(singer_name: string, song: string, auth_cookie?: string) {
this.singer_name=singer_name;
this.song=song;
this.auth_cookie = auth_cookie;
}
singer_name: string;
song: string; //Actually the ID of the Song
auth_cookie?: string; //The "cookie" to authenticate for changing an entry. Null otherwise
}
@@ -1,16 +0,0 @@
export class Genre {
constructor(id: number, name: string) {
this.id = id;
this._name = name;
}
public get name() : string {
return this._name;
}
id: number;
_name: string;
}
@@ -1,10 +0,0 @@
export class Language {
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
id: number;
name: string;
}
@@ -1,26 +0,0 @@
import { Genre } from './genre.model';
import { Language } from './language.model';
import { Artist } from './artist.model';
export class Song {
constructor(title: string, artist: Artist, karafun_id: number, duet: boolean, explicit: boolean, id: number, genres: Array<String>, languages: Array<String>) {
this.title=title;
this.artist=artist;
this.karafun_id=karafun_id;
this.duet=duet;
this.explicit=explicit;
this.id=id;
this.genres=genres;
this.languages=languages;
}
title: string;
artist: Artist;
karafun_id: number;
duet: boolean;
explicit: boolean;
id: number;
genres: Array<String>;
languages: Array<String>;
}
@@ -1,48 +0,0 @@
<div class="content">
<mat-form-field class="search-input">
<mat-label>Suche...</mat-label>
<mat-icon matSuffix svgIcon="magnify"></mat-icon>
<input type="text" matInput placeholder="Tippe einen Künstler oder Song..."
(keyup)="applyFilter($event.target.value)">
</mat-form-field>
</div>
<div class="resultcontainer">
<mat-list *ngFor="let song of songs">
<mat-card class="result">
<div class="card-left">
<mat-card-title>{{song.artist}} - {{song.title}}</mat-card-title>
<div class="song-info">
<div class="info-icons">
<ng-template [ngIf]="song.duet==true" [ngIfElse]="nonduet">
<mat-icon svgIcon="account-multiple"></mat-icon>
</ng-template>
<ng-template #nonduet>
<mat-icon svgIcon="account"></mat-icon>
</ng-template>
<ng-template [ngIf]="song.explicit==true" [ngIfElse]="nonexplicit">
<mat-icon svgIcon="alpha-e-box"></mat-icon>
</ng-template>
<ng-template #nonexplicit>
<mat-icon svgIcon="alpha-e-box" class="icon-disabled"></mat-icon>
</ng-template>
</div>
<div class="genre-list">
<mat-icon svgIcon="music-circle"></mat-icon>
<mat-chip-list *ngFor="let genre of song.genres">
<mat-chip>{{genre}}</mat-chip>
</mat-chip-list>
</div>
<div class="language-list">
<mat-icon svgIcon="account-voice"></mat-icon>
<mat-chip-list *ngFor="let language of song.languages">
<mat-chip>{{language}}</mat-chip>
</mat-chip-list>
</div>
</div>
</div>
<button mat-flat-button color="accent" class="add-button">
<mat-icon svgIcon="playlist-plus"></mat-icon>
</button>
</mat-card>
</mat-list>
</div>
@@ -1,120 +0,0 @@
.search-input {
width: 95%;
margin-top: 1rem;
}
.add-button {
height: 3rem;
}
.content {
overflow: hidden;
display: flex;
justify-content: center;
}
.resultcontainer {
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
}
.result {
margin-left: auto;
margin-right: auto;
width: 90%;
margin-top: 0.2rem;
display: flex;
align-items: stretch;
justify-content: space-between;
flex-direction: column;
}
.card-left {
display: flex;
flex-direction: column;
width: 100%;
}
.song-info {
box-sizing: border-box;
display: flex;
flex-direction: column;
width: 100%;
}
.song-info > * {
margin-right: 0.5rem;
}
.icon-disabled {
opacity: 0.1;
}
.genre-list > mat-icon {
padding-right: 0.25rem;
}
.genre-list mat-chip-list {
margin-right: 0.15rem;
}
.genre-list {
overflow-x: scroll;
overflow-y: hidden;
white-space: nowrap;
display: flex;
flex-direction: row;
padding-bottom: 0.25rem;
align-items: center;
-webkit-mask-image: linear-gradient(to right, rgba(0,0,0,0) 0%,rgba(0,0,0,1) 2px,rgba(0,0,0,1) calc(100% - 2px),rgba(0,0,0,0) 100%);
mask-image: linear-gradient(to right, rgba(0,0,0,0) 0%,rgba(0,0,0,1) 2px,rgba(0,0,0,1) calc(100% - 2px),rgba(0,0,0,0) 100%);
-ms-overflow-style: none;
}
.language-list > mat-icon {
padding-right: 0.25rem;
}
.language-list mat-chip-list {
margin-right: 0.15rem;
}
.genre-list::-webkit-scrollbar {
width: 0px;
height: 0px;
display: none;
}
.language-list {
overflow-x: scroll;
overflow-y: hidden;
white-space: nowrap;
display: flex;
flex-direction: row;
padding-bottom: 0.25rem;
align-items: center;
-ms-overflow-style: none;
}
.language-list::-webkit-scrollbar {
display: none;
}
$breakpoint-tablet: 768px;
@media (min-width: $breakpoint-tablet) {
.result {
flex-direction: row;
}
.add-button {
height: 6.5rem;
width: 4rem;
align-self: center;
}
}
@@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SongSearchComponent } from './song-search.component';
describe('SongSearchComponent', () => {
let component: SongSearchComponent;
let fixture: ComponentFixture<SongSearchComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ SongSearchComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SongSearchComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,43 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { SongServiceService } from "../song-service.service";
import { Song } from '../models/song.model';
import { Subject } from 'rxjs';
import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators';
@Component({
selector: 'app-song-search',
templateUrl: './song-search.component.html',
styleUrls: ['./song-search.component.scss']
})
export class SongSearchComponent implements OnInit {
private searchSub$ = new Subject<string>();
constructor(private songServiceService: SongServiceService) { }
songs: Array<Song> = new Array<Song>();
updateSongs(text: string) {
this.songServiceService.searchSongByText(text).subscribe(x => {
console.log(x);
this.songs = x;
});
}
applyFilter(filterValue: string) {
this.searchSub$.next(filterValue)
}
ngOnInit(): void {
this.searchSub$.pipe(
debounceTime(400),
distinctUntilChanged()
).subscribe((filterValue: string) => {
this.updateSongs(filterValue);
});
//Für Testzwecke
this.updateSongs("Test");
}
}
@@ -1,16 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { SongServiceService } from './song-service.service';
describe('SongServiceService', () => {
let service: SongServiceService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(SongServiceService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -1,42 +0,0 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from "@angular/common/http";
import { Song } from './models/song.model';
import { Artist } from './models/artist.model';
import { Genre } from './models/genre.model';
import { Language } from './models/language.model';
import { RuntimeConfigLoaderService } from 'runtime-config-loader';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class SongServiceService {
private api: string;
constructor(
private http: HttpClient,
private configSvc: RuntimeConfigLoaderService
) {
this.api=configSvc.getConfigObjectKey("api");
}
searchSongByText(text: string): Observable<Array<Song>> {
let out = new Array<Song>();
this.http.get(this.api +"/songs/compl?search="+text).subscribe((data: Observable<JSON>) => {
data.forEach(element => {
out.push(new Song(element["title"],element["artist"],element["karafun_id"],element["duo"],element["explicit"],element["_id"],element["styles"],element["languages"]));
});
});
const observable = new Observable<Array<Song>>( subscriber => {
subscriber.next(out);
})
return observable;
}
}
@@ -1,3 +0,0 @@
{
"api":"http://localhost:5000/api"
}

Some files were not shown because too many files have changed in this diff Show More