Compare commits

...

218 Commits

Author SHA1 Message Date
0be1ec655a Update devcontainer.json 2023-10-10 04:03:04 +00:00
1fa276d8c1 Adapt Anngular frontend to karafun terminology 2023-10-03 17:22:39 +00:00
85497a1569 Improve search 2023-10-03 17:21:30 +00:00
ff8c113bfc Evrything is working.
Existing parts of frontend can access backend.
2023-09-30 00:43:19 +00:00
2c549c7c59 Now everything is actually working again. 2023-09-30 00:21:58 +00:00
2aec7a3cff Everything working. 2023-09-29 23:37:34 +00:00
76a17874ef Add frontend launch config 2023-09-29 22:51:38 +00:00
2cf48e5904 Merge branch 'master' of github.com:PhoenixTwoFive/karaoqueue 2023-09-29 22:49:56 +00:00
554ea16b9a Update dependencies 2023-09-29 22:49:49 +00:00
d35b65ac12 Update dependencies 2023-09-29 22:48:56 +00:00
38d201b609 Update 2023-09-29 22:46:55 +00:00
362923c5c2 Update 2023-09-29 22:46:06 +00:00
b9865ca936 Update 2023-09-29 22:44:58 +00:00
2f408ff6ed Add empty test.ts 2023-09-29 22:40:35 +00:00
13ead16ffe Add angular CLI to devcontainer 2023-09-29 22:40:08 +00:00
96511f169e Merge branch 'legacy' 2023-09-29 21:49:47 +00:00
8f926621c1 Merge pull request #62 from PhoenixTwoFive/dependabot/pip/backend/flask-2.3.2
Bump flask from 2.2.3 to 2.3.2 in /backend
2023-09-29 23:33:02 +02:00
a8e1a8f647 Merge pull request #67 from PhoenixTwoFive/dependabot/pip/backend/requests-2.31.0
Bump requests from 2.28.2 to 2.31.0 in /backend
2023-09-29 23:32:42 +02:00
e8e0bca648 Update devcontainer.json 2023-09-22 16:55:05 +00:00
5efa21924b Update dockerfile to adapt to new debian version 2023-06-30 15:59:04 +02:00
4c806c3550 update docker-compose container version 2023-06-30 15:58:34 +02:00
c21e6300e9 Merge pull request #70 from PhoenixTwoFive/64-defaults-for-entry-limitation-too-high
lowered maxqueue to 10 and participant quota to 2
2023-06-30 15:34:56 +02:00
7525708dce lowered maxqueue to 10 and participant quota to 2 2023-06-30 15:32:13 +02:00
37d95f61b2 Merge pull request #69 from PhoenixTwoFive/63-input-sanitization
63 input sanitization
2023-06-30 15:29:46 +02:00
a54953ff0d fix copy functionality
Fixed copy functionality by no longer rendering the data into the
function call as string literal, but instead accessing it from the DOM
in the function call.
2023-06-30 15:28:46 +02:00
349eff9a09 strip whitespace from participant name in enqueue endpoint 2023-06-30 14:50:32 +02:00
b7a79462dc Add pipfile 2023-06-30 00:17:44 +02:00
0a038029f7 Merge pull request #68 from PhoenixTwoFive/61-properly-set-focus-on-opening-enqueue-modal
Set focus on singer name input when modal is shown
2023-06-30 00:16:09 +02:00
93e6606d6d Set focus on singer name input when modal is shown 2023-06-30 00:13:48 +02:00
bd3bec8c4f Bump requests from 2.28.2 to 2.31.0 in /backend
Bumps [requests](https://github.com/psf/requests) from 2.28.2 to 2.31.0.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.28.2...v2.31.0)

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-02 00:33:58 +00:00
1faa2614fa Merge branch 'legacy' of github.com:PhoenixTwoFive/karaoqueue into legacy 2023-04-28 13:41:51 +02:00
93c8a2cb7b Update Version 2023-04-28 13:41:39 +02:00
ddb1e0d2a0 Merge pull request #58 from PhoenixTwoFive:fix/legacy/fix-no-owned-entry
Fix error on no owned entries
2023-04-28 13:40:28 +02:00
e66e4a6c19 Fix error on no owned entries 2023-04-28 13:39:20 +02:00
08d0e5557b Update docker-compose to new version 2023-04-28 13:13:47 +02:00
a987dfc9d2 Merge branch 'legacy' of github.com:PhoenixTwoFive/karaoqueue into legacy 2023-04-28 13:03:57 +02:00
04511a91a1 Freeze versions to avoid Flask 2.3 deprecation problems 2023-04-28 13:03:53 +02:00
7da0dc19dc Merge pull request #57 from PhoenixTwoFive/feature/legacy/56-buttons-for-copying-to-clipboard-in-entry-list
Feature/legacy/56 buttons for copying to clipboard in entry list
2023-04-28 12:56:36 +02:00
d0d8e41b48 Indicate copyability of table text
Indicate copyability on click of table contents by highlighting
hovered element
2023-04-28 12:53:03 +02:00
f2b4611ea6 Remove problematic tooltip and debug log 2023-04-28 12:52:03 +02:00
b76fcfd8e4 Add Copy functionality and corresponding toasts 2023-04-28 12:40:28 +02:00
c9cbd24569 Merge pull request #55 from PhoenixTwoFive/feature/legacy/53-löschung-von-eigenen-einträgen-erlauben
Feature/legacy/53 löschung von eigenen einträgen erlauben
2023-04-27 00:18:37 +02:00
add528fb80 Löschung eigener Enträge implementiert. 2023-04-26 19:20:21 +02:00
865df5d588 Implement EventID to scope ClientIDs and Entry IDs
Implement an EventID saved in settings. Currently this is used to scope
clientIDs and entryIDs to an event. The client checks the event currently going on on
the server, and discards its localstorage (containing the clientID) if
it has changed
2023-04-26 18:08:03 +02:00
adebf35d08 Update lint.yaml
Remove action run on every push
2023-04-25 16:52:36 +02:00
d2caaac4bc Codecheck (#54)
* Add GitHub Action

* Add Linting

* Add .editorconfig
2023-04-25 16:46:43 +02:00
58dd0dd93b Add devcontainer.json setup 2023-04-02 17:21:56 +02:00
10717e753b Update Compose File 2023-03-31 20:58:47 +02:00
f32f02dc44 Fix Sorting in List view 2023-03-31 20:57:34 +02:00
1dd79b3be3 Merge pull request #44 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/loader-utils-2.0.4
Bump loader-utils from 2.0.2 to 2.0.4 in /frontend/ng-karaoqueue
2023-03-31 11:47:18 +02:00
e4dec0393c Merge pull request #43 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/http-cache-semantics-4.1.1
Bump http-cache-semantics from 4.1.0 to 4.1.1 in /frontend/ng-karaoqueue
2023-03-31 11:47:02 +02:00
a0eacf5d13 Merge pull request #42 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/ua-parser-js-0.7.33
Bump ua-parser-js from 0.7.31 to 0.7.33 in /frontend/ng-karaoqueue
2023-03-31 11:46:48 +02:00
d7c8d9cfd9 Merge pull request #41 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/json5-2.2.3
Bump json5 from 2.2.1 to 2.2.3 in /frontend/ng-karaoqueue
2023-03-31 11:46:34 +02:00
08314e1fb1 Merge pull request #40 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/engine.io-6.2.1
Bump engine.io from 6.2.0 to 6.2.1 in /frontend/ng-karaoqueue
2023-03-31 11:46:08 +02:00
3921a9ea76 Update docker-Compose 2023-03-30 22:56:09 +02:00
8c735866a3 Remove remnants of file based config 2023-03-30 22:45:08 +02:00
035394c36b Rephrase Information text 2023-03-30 22:37:39 +02:00
1d4f77ea19 Merge pull request #49 from PhoenixTwoFive/PhoenixTwoFive/issue45
Inform user that entries are closed when big button on main page is pressed and disabled
2023-03-30 22:34:52 +02:00
573f58d764 Inform user that entries are closed when big button on main page is pressed and disabled
Fixes #45
2023-03-30 22:33:50 +02:00
dff46404f6 Merge pull request #48 from PhoenixTwoFive:bugfix/47-misaligned-buttons
Fix button alignment
2023-03-30 22:28:57 +02:00
4fd58bc39f Fix button alignment 2023-03-30 22:28:12 +02:00
bf97e9e5e4 Fix container version 2023-03-30 22:21:42 +02:00
e6a3cd43b4 Bump loader-utils from 2.0.2 to 2.0.4 in /frontend/ng-karaoqueue
Bumps [loader-utils](https://github.com/webpack/loader-utils) from 2.0.2 to 2.0.4.
- [Release notes](https://github.com/webpack/loader-utils/releases)
- [Changelog](https://github.com/webpack/loader-utils/blob/v2.0.4/CHANGELOG.md)
- [Commits](https://github.com/webpack/loader-utils/compare/v2.0.2...v2.0.4)

---
updated-dependencies:
- dependency-name: loader-utils
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-30 19:21:22 +00:00
e08041338a Add persistent volume to mariadb container. 2023-03-30 21:18:40 +02:00
4c64144f3d Fix sticky Popups 2023-03-30 21:05:36 +02:00
fad21ba6c5 Fix DELETE statements 2023-03-30 19:30:27 +02:00
3f09b79844 Fix Dockerfile for "new" WSGI server 2023-03-30 19:30:13 +02:00
1bfe3b5d4b Update Dockerfile 2023-03-30 19:14:24 +02:00
f055a59a38 Fix Dockerfile with in-Container update 2023-03-30 18:14:40 +02:00
dc53d8a8b1 Fix Typos 2023-03-30 18:00:23 +02:00
fe71fa2d8c Fix Caching 2023-03-30 18:00:08 +02:00
7ef938a5ff Create new config with credentials from env vars 2023-03-30 17:51:20 +02:00
12207c1246 Admin credentials can be changed via settings 2023-03-30 17:39:44 +02:00
16cb9e7d5a Fix theme setting persistence 2023-03-30 17:39:13 +02:00
429ffddced Update data_adapters for sqlalchemy upgrade 2023-03-30 17:38:34 +02:00
ca73d57567 Update Requirements 2023-03-30 17:07:01 +02:00
cf6d586856 Overhaul database code 2023-03-30 17:06:52 +02:00
24458a78d0 Fix bugs 2023-03-30 17:06:36 +02:00
a2cf4fc47f Change .env.dev to reflect new settings storage 2023-03-30 17:05:46 +02:00
e84ff1a381 Change container registry to github 2023-03-30 17:05:17 +02:00
79156c539a Better Dark Mode 2023-03-30 04:43:53 +02:00
757bfa2483 Fix Dark Mode 2023-03-30 03:28:23 +02:00
d56eb609b9 Fix entry button alignment 2023-03-30 03:25:02 +02:00
6d9541f0bd Add Dark Mode 2023-03-30 03:24:45 +02:00
ee4774159c Update Python to 3.10 2023-03-30 03:23:22 +02:00
6d084ee83c Add theming 2023-03-30 02:57:21 +02:00
971548189f Remove redundant save button from settings 2023-03-30 02:26:24 +02:00
da8ad57293 Fix Settings 2023-03-30 02:22:59 +02:00
98981b1e1e Move config to database 2023-03-30 02:22:46 +02:00
d33006251e Remove inline env from docker-compose.prod.yml 2023-03-30 02:22:02 +02:00
2a8040642a Merge branch 'legacy' into feature/legacy/mariadb_database 2023-03-28 01:26:24 +02:00
cbf7aad8ac Fix Type errors 2023-03-28 01:25:33 +02:00
d5a21b82de Add GitHub Link to footer 2023-03-28 01:00:41 +02:00
551536bcb4 Update bootstrap-tables 2023-03-28 01:00:21 +02:00
b8220732ee Introduce "dev environment" and dotenv
There are now two dockerfiles. One for production, one for development.
All configuration is now also handled through dotenv files,
which these dotenv files, as well as the included VSCode launch tasks
use.
2023-03-28 00:54:04 +02:00
831166f38b Intall dependencies using requirements.txt 2023-03-27 23:38:22 +02:00
41a24ad9ce readd main.py 2023-03-27 01:02:26 +02:00
0ff01af411 Bump http-cache-semantics from 4.1.0 to 4.1.1 in /frontend/ng-karaoqueue
Bumps [http-cache-semantics](https://github.com/kornelski/http-cache-semantics) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/kornelski/http-cache-semantics/releases)
- [Commits](https://github.com/kornelski/http-cache-semantics/compare/v4.1.0...v4.1.1)

---
updated-dependencies:
- dependency-name: http-cache-semantics
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-04 17:11:27 +00:00
b1964ffb55 Bump ua-parser-js from 0.7.31 to 0.7.33 in /frontend/ng-karaoqueue
Bumps [ua-parser-js](https://github.com/faisalman/ua-parser-js) from 0.7.31 to 0.7.33.
- [Release notes](https://github.com/faisalman/ua-parser-js/releases)
- [Changelog](https://github.com/faisalman/ua-parser-js/blob/master/changelog.md)
- [Commits](https://github.com/faisalman/ua-parser-js/compare/0.7.31...0.7.33)

---
updated-dependencies:
- dependency-name: ua-parser-js
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-27 11:01:38 +00:00
4e863ecd34 Bump json5 from 2.2.1 to 2.2.3 in /frontend/ng-karaoqueue
Bumps [json5](https://github.com/json5/json5) from 2.2.1 to 2.2.3.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v2.2.1...v2.2.3)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-09 08:02:28 +00:00
a63aad78b0 Bump engine.io from 6.2.0 to 6.2.1 in /frontend/ng-karaoqueue
Bumps [engine.io](https://github.com/socketio/engine.io) from 6.2.0 to 6.2.1.
- [Release notes](https://github.com/socketio/engine.io/releases)
- [Changelog](https://github.com/socketio/engine.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/engine.io/compare/6.2.0...6.2.1)

---
updated-dependencies:
- dependency-name: engine.io
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-22 06:38:44 +00:00
6efb234a1c Merge pull request #39 from PhoenixTwoFive/feature/legacy/mariadb_database
Feature/legacy/mariadb database
2022-10-08 19:51:56 +02:00
698c3717fd Remove outdated workflows 2022-10-08 19:50:35 +02:00
8dd90b728c MySQL Working, bug fixes. 2022-10-08 19:47:29 +02:00
580ede3bb0 Update 2022-09-24 03:48:53 +02:00
1a63232401 Update Everything 2022-09-24 03:07:39 +02:00
8183dcfcf4 Angular CLI update for packages - @angular/material@14 2022-09-24 03:05:13 +02:00
81b36e7f1a Update 2022-09-24 03:04:03 +02:00
3c3795e25a @angular/cli migration - update-angular-packages-version-prefix
Update Angular packages 'dependencies' and 'devDependencies' version prefix to '^' instead of '~'.
2022-09-24 02:56:05 +02:00
7a7a647be3 @angular/cli migration - remove-default-project-option
Remove 'defaultProject' option from workspace configuration. The project to use will be determined from the current working directory.
2022-09-24 02:56:01 +02:00
484b7cdfcd Angular CLI update for packages - @angular/core@14, @angular/cli@14 2022-09-24 02:56:01 +02:00
c8a7979132 @angular/cdk migration - migration-v13
Updates the Angular CDK to v13
2022-09-24 02:55:01 +02:00
45e5fd853e Angular CLI update for packages - @angular/material@13 2022-09-24 02:54:57 +02:00
5c66451524 Update 2022-09-24 02:54:17 +02:00
abc06d3181 @angular/cli migration - update-angular-config-v13
Remove deprecated options from 'angular.json' that are no longer present in v13.
2022-09-24 02:51:08 +02:00
2b4cabcb98 Angular CLI update for packages - @angular/core@13, @angular/cli@13 2022-09-24 02:51:07 +02:00
b3670aa0e0 Update @angular/material and @angular/cdk to 12.x 2022-09-24 02:47:31 +02:00
ba982001ba Update @angular/material and @angular/cdk to 11.x 2022-09-24 02:45:04 +02:00
d1ec708780 Update @angular/material and @angular/cdk to 10.x 2022-09-24 02:43:28 +02:00
f901708896 Remove runtime-config-loader 2022-09-24 02:26:37 +02:00
f0a7ee2d2b @angular/cli migration - update-zonejs
Update 'zone.js' to version 0.11.x. Read more about this here: https://github.com/angular/angular/blob/master/packages/zone.js/CHANGELOG.md#breaking-changes-since-zonejs-v0111
2022-09-24 02:24:19 +02:00
bc5818f50d @angular/cli migration - update-angular-config-v12
Remove deprecated options from 'angular.json' that are no longer present in v12.
2022-09-24 02:24:15 +02:00
a1264cb15b Angular CLI update for packages - @angular/core@12, @angular/cli@12 2022-09-24 02:24:15 +02:00
2e1eab3e2a Update 2022-09-24 02:22:34 +02:00
b69e771b75 Update 2022-09-24 02:22:25 +02:00
7d8ae1167e Update package-lock.json 2022-09-24 02:18:40 +02:00
b9e0302443 Delete package-lock.json 2022-09-24 02:14:48 +02:00
bb25b2801b Remove unused package 2022-09-23 23:20:09 +02:00
4d75c125ba Upgrade Angular to 10.x 2022-09-23 23:13:57 +02:00
5050e0cf60 Merge branch 'legacy' into new_frontend 2022-09-23 22:38:30 +02:00
a22960eae9 Fix DB Connection 2022-07-15 15:54:15 +02:00
bc44985fed Fix DBCONNSTRING 2022-07-15 15:35:58 +02:00
72914109c0 prune requirements 2022-07-15 15:33:42 +02:00
57ced69ddd Load DB Connection from ENV 2022-07-15 15:27:36 +02:00
fc78bdc4fe Fix types 2022-07-15 15:09:48 +02:00
adfd120636 Update package names for 22.04 2022-07-15 15:01:07 +02:00
67a9552fee Update ubuntu to get newer mariadb connector 2022-07-15 14:55:14 +02:00
3ae2803fcc Remove mysql and install mariadb 2022-07-15 14:51:05 +02:00
2138189dff python3 is still not default on Ubuntu? 2022-07-15 14:44:26 +02:00
9907a19066 Test 2022-07-15 14:43:16 +02:00
2a18f3dc2c Completely remove mariadb libs to hopefully fix conflicts 2022-07-15 14:36:47 +02:00
69e252e28a Add missing libraries to install 2022-07-15 14:35:52 +02:00
8151e615c0 Test 2022-07-15 14:34:07 +02:00
2a92c39450 Test 2022-07-15 14:32:51 +02:00
b88aac69e6 Fix.. 2022-07-15 14:32:06 +02:00
c9ad755a04 Fix? 2022-07-15 14:29:18 +02:00
3527ba4fd9 Fix dependencies 2022-07-15 14:27:39 +02:00
fb9677dc88 Fix github workflow 2022-07-15 14:23:17 +02:00
97dd80b03a Add or update the Azure App Service build and deployment workflow config 2022-07-15 14:16:06 +02:00
faad60346f Intial changes for MariaDB 2022-07-15 14:01:02 +02:00
3be307c0f9 Short commit hash on heroku 2022-07-15 02:59:33 +02:00
8cc9ed3645 Add version loading 2022-07-15 02:55:18 +02:00
108d70253c TEst 2022-07-15 02:34:41 +02:00
527c9d451d Refactor for heroku? 2022-07-15 02:31:58 +02:00
5beb752edf forgot gunicorn 2022-07-15 02:24:35 +02:00
589e50120b Refactor for heroku... 2022-07-15 02:22:36 +02:00
f421423636 Refactor for heroku 2022-07-15 02:13:41 +02:00
f6901a6175 Change 2022-07-15 02:11:12 +02:00
895dd14073 Refactor for heroku 2022-07-15 02:07:40 +02:00
95838885e7 Fix parsing of songlist download page for CSV file link 2022-04-07 21:32:11 +02:00
84badb0e13 Retrieve old back end 2022-03-15 19:45:49 +01:00
10613d5c67 update package-lock.json 2022-03-07 18:33:56 +01:00
5bcf73a871 Merge pull request #32 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/jszip-3.7.1
Bump jszip from 3.2.2 to 3.7.1 in /frontend/ng-karaoqueue
2022-03-07 18:15:32 +01:00
645052027c Merge pull request #31 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/color-string-1.9.0
Bump color-string from 1.5.3 to 1.9.0 in /frontend/ng-karaoqueue
2022-03-07 18:15:20 +01:00
bf96424853 Merge pull request #30 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/karma-6.3.16
Bump karma from 4.4.1 to 6.3.16 in /frontend/ng-karaoqueue
2022-03-07 18:15:10 +01:00
d0e9e31a8c Merge pull request #29 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/url-parse-1.5.10
Bump url-parse from 1.4.7 to 1.5.10 in /frontend/ng-karaoqueue
2022-03-07 18:15:00 +01:00
415758b94d Merge pull request #27 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/follow-redirects-1.14.8
Bump follow-redirects from 1.9.0 to 1.14.8 in /frontend/ng-karaoqueue
2022-03-07 18:14:50 +01:00
89fca235ed Merge pull request #23 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/tar-4.4.19
Bump tar from 4.4.13 to 4.4.19 in /frontend/ng-karaoqueue
2022-03-07 18:14:28 +01:00
020a06d381 Merge pull request #20 from PhoenixTwoFive/dependabot/npm_and_yarn/backend/karaoqueue-backend/path-parse-1.0.7
Bump path-parse from 1.0.6 to 1.0.7 in /backend/karaoqueue-backend
2022-03-07 18:14:06 +01:00
e09a9db75f Merge pull request #19 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/path-parse-1.0.7
Bump path-parse from 1.0.6 to 1.0.7 in /frontend/ng-karaoqueue
2022-03-07 18:13:50 +01:00
654b8743d1 Merge pull request #17 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/dns-packet-1.3.4
Bump dns-packet from 1.3.1 to 1.3.4 in /frontend/ng-karaoqueue
2022-03-07 18:13:37 +01:00
3d8dfc9fe2 Bump jszip from 3.2.2 to 3.7.1 in /frontend/ng-karaoqueue
Bumps [jszip](https://github.com/Stuk/jszip) from 3.2.2 to 3.7.1.
- [Release notes](https://github.com/Stuk/jszip/releases)
- [Changelog](https://github.com/Stuk/jszip/blob/master/CHANGES.md)
- [Commits](https://github.com/Stuk/jszip/compare/v3.2.2...v3.7.1)

---
updated-dependencies:
- dependency-name: jszip
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-07 17:13:18 +00:00
a642beee31 Merge pull request #16 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/hosted-git-info-2.8.9
Bump hosted-git-info from 2.8.8 to 2.8.9 in /frontend/ng-karaoqueue
2022-03-07 18:13:13 +01:00
8d42db22b4 Bump color-string from 1.5.3 to 1.9.0 in /frontend/ng-karaoqueue
Bumps [color-string](https://github.com/Qix-/color-string) from 1.5.3 to 1.9.0.
- [Release notes](https://github.com/Qix-/color-string/releases)
- [Changelog](https://github.com/Qix-/color-string/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Qix-/color-string/commits/1.9.0)

---
updated-dependencies:
- dependency-name: color-string
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-07 17:13:10 +00:00
d7de33063e Merge pull request #15 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/lodash-4.17.21
Bump lodash from 4.17.19 to 4.17.21 in /frontend/ng-karaoqueue
2022-03-07 18:13:02 +01:00
607abc28f4 Merge pull request #13 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/ssri-6.0.2
Bump ssri from 6.0.1 to 6.0.2 in /frontend/ng-karaoqueue
2022-03-07 18:12:50 +01:00
4a1a2df725 Merge pull request #12 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/y18n-4.0.1
Bump y18n from 4.0.0 to 4.0.1 in /frontend/ng-karaoqueue
2022-03-07 18:12:35 +01:00
7cc614b496 Merge pull request #11 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/elliptic-6.5.4
Bump elliptic from 6.5.3 to 6.5.4 in /frontend/ng-karaoqueue
2022-03-07 18:12:14 +01:00
e4de261dfa Bump karma from 4.4.1 to 6.3.16 in /frontend/ng-karaoqueue
Bumps [karma](https://github.com/karma-runner/karma) from 4.4.1 to 6.3.16.
- [Release notes](https://github.com/karma-runner/karma/releases)
- [Changelog](https://github.com/karma-runner/karma/blob/master/CHANGELOG.md)
- [Commits](https://github.com/karma-runner/karma/compare/v4.4.1...v6.3.16)

---
updated-dependencies:
- dependency-name: karma
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-02 06:40:14 +00:00
7e66a42f9a Bump url-parse from 1.4.7 to 1.5.10 in /frontend/ng-karaoqueue
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.4.7 to 1.5.10.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.4.7...1.5.10)

---
updated-dependencies:
- dependency-name: url-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-26 21:21:08 +00:00
f4c9b8d26a Bump follow-redirects from 1.9.0 to 1.14.8 in /frontend/ng-karaoqueue
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.9.0 to 1.14.8.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.9.0...v1.14.8)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-13 02:28:48 +00:00
99c7917573 Merge branch 'legacy_feature_transferred_tagging' into legacy 2021-10-09 19:37:53 +02:00
23e7bf71bc Add transfer status toggle button 2021-10-09 19:36:28 +02:00
360b61fe21 Better version identification 2021-10-09 18:16:55 +02:00
bf1b711abe Fix webmanifest 2021-10-09 01:04:23 +02:00
65635f57a8 Fix overly agressive caching 2021-10-09 01:04:15 +02:00
013dcecba9 fix launch configs 2021-10-09 00:03:56 +02:00
d79eebc949 Add favicon, fix caching 2021-10-09 00:03:46 +02:00
c203317320 Add Version identification to footer 2021-10-08 23:21:41 +02:00
4b70b70a1d Merge branch 'legacy_feature_client_identification_queue_limit' into stable 2021-10-08 22:44:32 +02:00
69f84332e6 style fixes 2021-10-06 02:16:24 +02:00
6a7b07ac9f data type error in config variables 2021-10-06 02:02:23 +02:00
ba4b5fb57c Bessere Formulierung von Fehlern 2021-10-06 02:00:09 +02:00
e6bc974a6f Eintragungstoggle fix 2021-10-06 01:59:51 +02:00
569daf0a04 Einstellungsseite hinzugefügt 2021-10-06 01:59:30 +02:00
78a39b8d17 Add client ids and quota as well as queue limit 2021-10-06 00:45:57 +02:00
14ce3e07f5 Force no scaling 2021-10-05 22:49:00 +02:00
573b7a69da Bump tar from 4.4.13 to 4.4.19 in /frontend/ng-karaoqueue
Bumps [tar](https://github.com/npm/node-tar) from 4.4.13 to 4.4.19.
- [Release notes](https://github.com/npm/node-tar/releases)
- [Changelog](https://github.com/npm/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-tar/compare/v4.4.13...v4.4.19)

---
updated-dependencies:
- dependency-name: tar
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-02 03:50:18 +00:00
8f769ac7e3 Configure HTTP->HTTPS redirect 2021-08-20 16:58:35 +02:00
4b2be56b3f Cosmetics 2021-08-20 15:58:45 +02:00
87a632a661 Fix breakage by not pinning versions 2021-08-20 15:47:30 +02:00
9ab1e9b841 Adapt for Google Cloud Deployment 2021-08-20 15:47:03 +02:00
da505e93a3 Bump path-parse from 1.0.6 to 1.0.7 in /backend/karaoqueue-backend
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-12 12:15:17 +00:00
ea7fba17a2 Bump path-parse from 1.0.6 to 1.0.7 in /frontend/ng-karaoqueue
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-11 19:21:03 +00:00
f3bf925e3d Bump dns-packet from 1.3.1 to 1.3.4 in /frontend/ng-karaoqueue
Bumps [dns-packet](https://github.com/mafintosh/dns-packet) from 1.3.1 to 1.3.4.
- [Release notes](https://github.com/mafintosh/dns-packet/releases)
- [Changelog](https://github.com/mafintosh/dns-packet/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mafintosh/dns-packet/compare/v1.3.1...v1.3.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-28 09:07:27 +00:00
d84eea9683 Bump hosted-git-info from 2.8.8 to 2.8.9 in /frontend/ng-karaoqueue
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-11 09:15:19 +00:00
48b9bc843d Bump lodash from 4.17.19 to 4.17.21 in /frontend/ng-karaoqueue
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.19 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.19...4.17.21)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-10 21:15:40 +00:00
a9e16742e0 Bump ssri from 6.0.1 to 6.0.2 in /frontend/ng-karaoqueue
Bumps [ssri](https://github.com/npm/ssri) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/npm/ssri/releases)
- [Changelog](https://github.com/npm/ssri/blob/v6.0.2/CHANGELOG.md)
- [Commits](https://github.com/npm/ssri/compare/v6.0.1...v6.0.2)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-30 14:24:28 +00:00
02a66ba522 Bump y18n from 4.0.0 to 4.0.1 in /frontend/ng-karaoqueue
Bumps [y18n](https://github.com/yargs/y18n) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/yargs/y18n/releases)
- [Changelog](https://github.com/yargs/y18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yargs/y18n/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-31 08:59:19 +00:00
f61df3e5ef Bump elliptic from 6.5.3 to 6.5.4 in /frontend/ng-karaoqueue
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.3 to 6.5.4.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.3...v6.5.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-10 15:08:24 +00:00
080648d587 Add node_modules to .gitignore 2020-08-14 12:46:14 +02:00
986d898673 Properly pass user input to SQLite and fix searches with umlauts.
Resolves #3.
2020-03-10 20:08:31 +01:00
b4418a05b6 Update vscode launch definitins 2020-03-10 18:52:56 +01:00
3ccd64bfbc Remove frontend stub from this branch 2020-03-10 18:52:45 +01:00
824184ea42 Fix CSS
Resolves #2
2020-03-06 19:37:51 +01:00
7325e901f2 Remove ugly scrolbars wen logged in 2020-03-06 19:37:32 +01:00
d26c877a52 Fix song database update
Resolves #1
2020-03-06 19:37:00 +01:00
96 changed files with 24430 additions and 13278 deletions

View File

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

36
.editorconfig Normal file
View File

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

12
.env.dev Normal file
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
.flake8 Normal file
View File

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

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

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

14
.gitignore vendored
View File

@ -87,7 +87,7 @@ ipython_config.py
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
Pipfile.lock
# celery beat schedule file
celerybeat-schedule
@ -131,3 +131,15 @@ dmypy.json
# Test data
data/
# Node Modules
node_modules/
# Version identification file
.version
**/.angular/
# Docker secrets
secrets.yml

71
.vscode/launch.json vendored
View File

@ -4,43 +4,90 @@
// 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)",
"command": "npm start",
"name": "Run npm start",
"request": "launch",
"type": "node-terminal",
"cwd": "${workspaceFolder}/frontend/ng-karaoqueue"
},
{
"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
},

16
.vscode/settings.json vendored
View File

@ -1,9 +1,15 @@
{
"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"
}
}

13
.vscode/tasks.json vendored
View File

@ -4,12 +4,25 @@
"version": "2.0.0",
"tasks": [
{
"label": "versiondump",
"type": "shell",
"command": "echo \"$(git rev-parse --abbrev-ref HEAD)-$(git describe)\"> ${workspaceFolder}/backend/.version",
"problemMatcher": []
},
{
"label": "Start Frontend",
"type": "npm",
"script": "start",
"path": "frontend/ng-karaoqueue/",
"problemMatcher": [
"$tsc"
]
},
{
"label": "mariadb",
"type": "shell",
"command": "docker-compose -f docker-compose.yml up --remove-orphans",
"isBackground": true,
}
]
}

20
Dockerfile Normal file
View File

@ -0,0 +1,20 @@
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
COPY ./backend /app

19
backend/.gcloudignore Normal file
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

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"
}
]
}

View File

@ -1,3 +0,0 @@
{
"python.pythonPath": "pyenv/bin/python"
}

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
backend/Pipfile Normal file
View File

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

1
backend/Procfile Normal file
View File

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

330
backend/app.py Normal file
View File

@ -0,0 +1,330 @@
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 flask_cors import CORS
app = Flask(__name__, static_url_path='/static')
CORS(app)
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())
else:
return render_template('main.html', list=database.get_list(), auth=basic_auth.authenticate())
@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:
print(request.data)
abort(400)
client_id = request.json['client_id']
if not helpers.is_valid_uuid(client_id):
print(request.data)
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:
print(request.data)
abort(400)
client_id = request.json['client_id']
if not helpers.is_valid_uuid(client_id):
print(request.data)
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())
@app.route("/settings")
@nocache
@basic_auth.required
def settings():
return render_template('settings.html', app=app, auth=basic_auth.authenticate(), themes=helpers.get_themes())
@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())
@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())
@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/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:
print(request.data)
abort(400)
client_id = request.json['client_id']
if not helpers.is_valid_uuid(client_id):
print(request.data)
abort(400)
if database.get_raw_entry(entry_id)[3] != client_id: # type: ignore
print(request.data)
abort(403)
if database.delete_entry(entry_id):
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:
print(request.data)
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/played/clear")
@nocache
@basic_auth.required
def clear_played_songs():
if database.clear_played_songs():
return Response('{"status": "OK"}', mimetype='text/json')
else:
return Response('{"status": "FAIL"}', mimetype='text/json')
@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_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()
if __name__ == "__main__":
app.run(host='127.0.0.1', port=8080, debug=True)

19
backend/app.yaml Normal file
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
backend/data_adapters.py Normal file
View File

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

332
backend/database.py Normal file
View File

@ -0,0 +1,332 @@
# -*- 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)
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 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_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_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 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:
print(f"Setting config {key} to {value}")
with get_db_engine().connect() as conn:
conn.execute(text(
"INSERT INTO config (`Key`, `Value`) VALUES ( :par_key , :par_value) ON DUPLICATE KEY UPDATE `Value`= :par_value"),
{"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")

View File

@ -1,11 +0,0 @@
FROM tiangolo/uwsgi-nginx-flask:python3.7
RUN pip install requests
RUN pip install pandas
RUN pip install Flask-BasicAuth
RUN pip install bs4
COPY ../app /app

View File

@ -1,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"

View File

@ -1,12 +0,0 @@
version: '2'
services:
mongo:
image: mongo
restart: always
ports:
- "27017:27017"
backend:
depends_on:
- mongo
build: .

177
backend/helpers.py Normal file
View File

@ -0,0 +1,177 @@
import requests
from bs4 import BeautifulSoup
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 os.environ.get("SOURCE_VERSION"):
app.config['VERSION'] = os.environ.get("SOURCE_VERSION")[0:7] # type: ignore # noqa: E501
elif os.path.isfile(".version"):
with open('.version', 'r') as file:
data = file.read().replace('\n', '')
if data:
app.config['VERSION'] = data
else:
app.config['VERSION'] = ""
else:
app.config['VERSION'] = ""
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)

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

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

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"
}
}

View File

@ -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";
}
}

View File

@ -1,54 +0,0 @@
import { Controller, Get, Res, Post, Delete, Patch, Req } from "routing-controllers";
@Controller("/queue")
export class QueueController {
/*
* Fetch entry Queue content
*/
@Get()
getQueue(@Req() req: any, @Res() res: any) {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ placeholder: "//TODO fetch" }));
}
/*
* Add entry to Queue
*/
@Post()
addEntry(@Req() req: any, @Res() res: any) {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ placeholder: "//TODO add" }));
}
/*
*
*/
@Delete()
clearQueue(@Req() req: any, @Res() res: any) {
return "//TODO clear";
}
/*
*
*/
@Get("/:entry:id")
getEntry(@Req() req: any, @Res() res: any) {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ placeholder: "//TODO get" }));
}
/*
*
*/
@Patch("/:entry_id")
editEntry(@Req() req: any, @Res() res: any) {
return "//TODO edit"
}
@Delete("/:entry_id")
deleteEntry(@Req() req: any, @Res() res: any) {
return "//TODO delete"
}
}

View File

@ -1,39 +0,0 @@
import { Controller, Get, Param, QueryParam } from "routing-controllers";
@Controller("/rpc")
export class RpcController {
@Get("/start_event")
doStartEvent() {
return "//TODO start_event"
}
@Get("/end_event")
doEndEvent() {
return "//TODO end_event"
}
@Get("/enable_registration")
doEnableRegistration() {
return "//TODO enable_registration"
}
@Get("/disable_registration")
doDisableRegistration() {
return "//TODO disable_registration"
}
@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}`
}
}

View File

@ -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"
}
}

View File

@ -1,9 +0,0 @@
import { JsonController, Get } from "routing-controllers";
@JsonController()
export class StatisticsController {
@Get()
getStatistics() {
return "//TODO statistics"
}
}

View File

@ -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;

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;
}

View File

@ -1,5 +0,0 @@
interface DataStoredInToken {
_id: string;
}
export default DataStoredInToken;

View File

@ -1,6 +0,0 @@
interface User {
username: string;
password: string;
}
export default User;

View File

@ -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 });
}
}

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"
]
}

View File

@ -1,13 +0,0 @@
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended"
],
"jsRules": {},
"rules": {
"trailing-comma": [
false
]
},
"rulesDirectory": []
}

4
backend/main.py Normal file
View File

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

37
backend/requirements.txt Normal file
View File

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

View File

@ -0,0 +1,201 @@
:root {
/* Navbar */
--navbar-background-color: #343a40;
--navbar-text-color: rgba(255, 255, 255, .5);
--navbar-text-color-hover: rgba(255, 255, 255, .75);
--navbar-text-color-active: rgba(255, 255, 255, 1);
/* Common */
--background-color: #ffffff;
--background-color-var: #f5f5f5;
--text-color: #212529;
--text-color-var: #343a40;
/* Modals */
--modal-background-color: #ffffff;
--modal-separator-color: #dee2e6;
--modal-close-color: #212529;
/* Tables */
--table-border-color: #dee2e6;
/* Input */
--input-background-color: #ffffff;
/* Misc */
--copy-highlight-color: rgba(251, 255, 0, 0.6);
}
body {
padding-top: 5rem;
background-color: var(--background-color);
}
html,
body {
height: 100%;
}
.site {
height: auto;
min-height: 100%;
}
main {
padding-bottom: 60px;
/* Höhe des Footers */
}
.footer {
margin-top: -60px;
width: 100%;
height: 60px;
/* Set the fixed height of the footer here */
/*line-height: 60px; /* Vertically center the text there */
background-color: var(--background-color-var);
}
.topbutton {
width: 100%;
}
table td {
overflow: hidden;
text-overflow: ellipsis;
}
table.entries tbody tr[data-index="0"] {
background-color: #007bff80;
font-weight: 600;
}
table.entries tbody tr[data-index="1"] {
background-color: #007bff40;
font-weight: 500;
}
table.entries tbody tr[data-index="2"] {
background-color: #007bff20;
font-weight: 400;
}
table.entries tbody tr[data-index="3"] {
background-color: #007bff10;
}
table td:first-child {
max-width: 200px !important;
}
.fa-solid {
vertical-align: auto;
}
@media (min-width: 768px) {
.topbutton {
width: auto;
}
}
@media print {
body {
font-size: 1.3em;
}
.footer {
display: none !important;
}
.admincontrols {
display: none;
}
}
body {
background-color: var(--background-color);
color: var(--text-color);
}
.footer {
background-color: var(--background-color-var);
}
.modal-content {
background-color: var(--background-color);
color: var(--text-color);
}
.modal-header {
background-color: var(--background-color);
color: var(--text-color-var);
border-color: var(var(--modal-separator-color));
}
.modal-footer {
background-color: var(--background-color);
color: var(--text-color-var);
border-color: var(var(--modal-separator-color));
}
.form-control {
background-color: var(--input-background-color);
color: var(--text-color)
}
.form-control:focus {
background-color: var(--input-background-color);
color: var(--text-color)
}
.table td,
.table th {
border-color: var(--table-border-color)
}
.table thead th {
border-color: var(--table-border-color)
}
table td.buttoncell {
text-align: end;
}
.close {
color: var(--text-color)
}
pre {
color: var(--text-color-var)
}
#entrytable td>span:hover {
background-color: var(--copy-highlight-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;
/* Modals */
--modal-background-color: #121212;
--modal-separator-color: #232323;
--modal-close-color: #f5f5f5;
/* Tables */
--table-border-color: #232323;
/* Input */
--input-background-color: #343434;
}
}

View File

@ -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;
}

BIN
backend/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

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"
}]
}

182
backend/templates/base.html Normal file
View File

@ -0,0 +1,182 @@
<!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.21.2/dist/bootstrap-table.min.css">
<!-- Bootstrap-Toaster-->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-toaster/4.1.2/css/bootstrap-toaster.min.css"
integrity="sha512-kYPLvO+Bu+xttOhbQvxs9nx7XSdxrb2JexRxQ3CpJQ7EtmlkBsWyOjlinLgiLWeLxuupFYB4cPqLOo0gnBnzeQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.2.1/dist/css/bootstrap.min.css" integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" crossorigin="anonymous">
<!-- Custom styles for this template -->
<link href="static/css/style.css" rel="stylesheet">
<!-- 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">
<div class="container text-center py-3">
{% if not auth %}
<a href="/login" class="ml-1 mr-1"><i class="fas fa-sign-in-alt mr-1"></i><span>Login</span></a>
{% endif %}
<a href="https://github.com/PhoenixTwoFive/karaoqueue"
class="ml-1 mr-1"><i class="fab fa-github mr-1"></i><span>Github</span></a>
<span class="text-muted"> {{karaoqueue_version}} -&nbsp;2019-23 - Phillip
Kühne</span>
</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.2.1/dist/js/bootstrap.min.js" integrity="sha384-B0UglyR+jN6CkvvICOB2joaf5I4l3gm9GU6Hc1og6Ls7i6U/mkkaduKaBhlAXv9k" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/4.4.0/bootbox.min.js"
integrity="sha256-4F7e4JsAJyLUdpP7Q8Sah866jCOhv72zU5E8lIRER4w=" crossorigin="anonymous">
</script>
<script src="https://unpkg.com/bootstrap-table@1.21.2/dist/bootstrap-table.min.js"></script>
<script
src="https://unpkg.com/bootstrap-table@1.21.2/dist/extensions/auto-refresh/bootstrap-table-auto-refresh.min.js"></script>
<script src="https://gitcdn.github.io/bootstrap-toggle/2.2.2/js/bootstrap-toggle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-toaster/4.1.2/js/bootstrap-toaster.min.js"
integrity="sha512-Ur6jgeoP3jnn38C7oBzDqMLRb+wxG2PXLKqgx2vgQ1ePFvbJ28f9iQSJplHD0APFHELOeS/df+RPNeENFtLrYw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
{% block extrajs %}{% endblock %}
<script>
$(document).ready(function () {
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()
})
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;
}
</script>
</body>
</html>

View File

@ -0,0 +1,77 @@
{% 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-url="/api/queue"
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) {
console.log("Value: " + value + ", Row: " + row + ", Index: " + index)
console.log(row)
if (getOwnedEntries().includes(row.entry_ID)) {
return "<button type='button' class='btn btn-danger' data-toggle='tooltip' data-placement='top' title='Eintrag zurückziehen' onclick=\"event.stopPropagation();$(this).tooltip('dispose');requestDeletionAsUser("+row["entry_ID"]+")\"><i class='fas fa-trash'></i></button>"
}
return ""
}
function requestDeletionAsUser(id) {
bootbox.confirm("Wirklich den Eintrag zurückziehen? Das könnte zu einer langen Wartezeit führen!", function (result) {
if (result) {
payload = {
"client_id": localStorage.getItem("clientId"),
"entry_id": id
}
$.ajax({
url: "/api/entries/delete/"+id,
type: "POST",
data: JSON.stringify(payload),
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function(result) {
toast = {
title: "Erfolgreich zurückgezogen",
message: "Eintrag wurde gelöscht",
status: TOAST_STATUS.SUCCESS,
timeout: 5000
}
Toast.create(toast);
location.reload()
}
})
}
})
}
</script>
{% endblock %}

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-url="/api/queue" 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), // or JSON.stringify ({name: 'jonas'}),
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 %}

View File

@ -0,0 +1,102 @@
{% extends 'base.html' %}
{% block title %}Abspielliste{% endblock %}
{% block content %}
<div id="toolbar">
<button type="button" class="topbutton btn btn-danger" onclick="confirmDeleteAllEntries()"><i
class="fas fa-trash mr-2"></i>Abspielliste löschen</button>
<button type="button" class="topbutton btn btn-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 confirmDeleteAllEntries() {
bootbox.confirm({
message: "Wirklich Abspielliste löschen?<br>Stelle sicher, dass du sie vorher zwecks Abrechnung gedruckt und/oder heruntergeladen hast!",
buttons: {
confirm: {
label: 'Ja',
className: 'btn btn-danger'
},
cancel: {
label: 'Nein',
className: 'btn btn-secondary'
}
},
callback: function(result){
if (result) {
deleteAllEntries()
}
}
})
}
function deleteAllEntries() {
$.ajax({
type: 'GET',
url: '/api/played/clear',
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 %}

View File

@ -0,0 +1,41 @@
{% 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>
<details>
<summary>Current config:</summary>
<pre>{% for key, val in config.items() %}{{key}}: {{val}}<br>{% endfor %}</pre>
</details>
{% endblock %}
{% block extrajs %}
{% endblock %}

View File

@ -0,0 +1,150 @@
{% extends 'base.html' %}
{% block title %}Songsuche{% endblock %}
{% block content %}
<input class="form-control" id="filter" type="text" placeholder="Suchen...">
<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 Liste setzen</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="form-group">
<form id="nameForm">
<div class="modal-body">
<label for="singerNameInput">Sängername</label>
<input type="text" class="form-control" id="singerNameInput" placeholder="Max Mustermann"
required>
<input id="selectedId" name="selectedId" type="hidden" value="">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary" id="submitSongButton">Anmelden</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extrajs %}
<script>
$(document).ready(function () {
$("#filter").focus();
$("#filter").keyup(function () {
var value = $(this).val().toLowerCase();
//alert(value);
if (value.length >= 1) {
$.getJSON("/api/songs/compl", { search: value }, function (data) {
var items = [];
$.each(data, function (key, val) {
items.push("<tr><td>" + val[0] + `</td>
<td class='buttoncell'><button type='button'
class='btn btn-primary justify-content-center align-content-between enqueueButton'
data-toggle='modal'
data-target='#enqueueModal' onclick='setSelectedId(`+ val[1] + `)'><i
class="fas fa-plus"></i></button></td>
</tr>`)
});
$("#songtable").html("")
$(items.join("")).appendTo("#songtable");
entriesAccepted()
});
} else {
$("#songtable").html("")
}
});
$("#nameForm").submit(function (e) {
e.preventDefault();
submitModal();
});
$('#enqueueModal').on('shown.bs.modal', function (e) {
$("#singerNameInput").focus();
})
});
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) {
$("#selectedId").attr("value", id);
}
function submitModal() {
var name = $("#singerNameInput").val();
var id = $("#selectedId").attr("value");
enqueue(localStorage.getItem("clientId"),id, name, function (response) {
console.log(response);
entryID = response["entry_id"];
toast = {
title: "Erfolgreich eingetragen",
message: "Du wurdest erfolgreich eingetragen.",
status: TOAST_STATUS.SUCCESS,
timeout: 5000
}
Toast.create(toast);
console.log("Entry ID: " + entryID);
addEntry(entryID);
$("#enqueueModal").modal('hide');
window.location.href = '/#end';
}, function (response) {
bootbox.alert({
message: "Deine Eintragung konnte leider nicht vorgenommen werden.\nGrund: "+response.responseJSON.status,
});
entriesAccepted();
$("#enqueueModal").modal('hide');
});
}
{% 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 %}

23
docker-compose.prod.yml Normal file
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
docker-compose.yml Normal file
View File

@ -0,0 +1,15 @@
# This Compose file is for development only. It is not intended for production use.
# It only starts auxiliary services, such as a database, that are required for the
# application to run. The application itself is started separately, using the
# command "python -m flask run" or your favorite IDE.
# Useful for attaching a debugger to the application.
version: "3.9"
services:
db:
image: mariadb
restart: always
env_file: .env.dev
ports:
- "3306:3306"

View File

@ -9,41 +9,9 @@ servers:
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.
/api/enqueue:
post:
description: 'Add entry to Queue'
summary: 'Add entry to Queue'
summary: "Enqueue a song"
requestBody:
required: true
content:
@ -51,342 +19,215 @@ paths:
schema:
type: object
properties:
singer_name:
client_id:
type: string
name:
type: string
id:
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
description: "Song enqueued successfully"
'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.
description: "Bad request, JSON is required"
'423':
description: "Cannot enqueue, conditions not met"
/songs:
/api/queue:
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
summary: "Get the queue"
responses:
'200':
description: OK. An array of Songs according to the Query string.
description: "Queue retrieved successfully"
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:
type: object
/api/songs:
get:
summary: Statistics about the Database
summary: "Get the list of songs"
responses:
'200':
description: Statistics as JSON
description: "List of songs retrieved successfully"
content:
application/json:
schema:
type: object
properties:
num_songs:
type: integer
num_entries:
type: integer
type: array
items:
type: object
/api/songs/update:
get:
summary: "Update the list of songs"
responses:
'200':
description: "Songs updated successfully"
'400':
description: "Bad request"
/api/songs/compl:
get:
summary: "Get song completions"
parameters:
- name: search
in: query
required: false
schema:
type: string
responses:
'200':
description: "Song completions retrieved successfully"
content:
application/json:
schema:
type: array
items:
type: object
'400':
description: "Bad request"
/api/entries/delete/{entry_id}:
get:
summary: "Admin deletes an entry"
parameters:
- name: entry_id
in: path
required: true
schema:
type: string
responses:
'200':
description: "Entry deleted successfully"
'400':
description: "Entry deletion failed"
/auth/login:
post:
summary: Logs in and returns the authentication cookie
summary: "User deletes an entry"
parameters:
- name: entry_id
in: path
required: true
schema:
type: string
requestBody:
required: true
description: A JSON object containing the login and password.
content:
application/json:
schema:
$ref: '#/components/schemas/LoginRequest'
security: [] # no authentication
type: object
properties:
client_id:
type: string
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:
description: "Entry deleted successfully"
'400':
description: "Bad request, JSON is required"
'403':
description: "Forbidden, client_id mismatch"
/api/entries/delete:
post:
summary: "Delete multiple entries"
requestBody:
required: true
content:
application/json:
schema:
type: array
items:
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.
description: "Entries deleted successfully"
'400':
description: "Bad request, JSON is required"
/rpc/end_event:
/api/entries/mark_sung/{entry_id}:
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.
summary: "Mark an entry as sung"
parameters:
- in: query
name: entry_id
- name: entry_id
in: path
required: true
schema:
type: string
description: The id of the entry to mark as done.
responses:
'200':
description: "Entry marked as sung successfully"
'400':
description: "Marking entry as sung failed"
/api/entries/mark_transferred/{entry_id}:
get:
summary: "Toggle transferred status of an entry"
parameters:
- name: entry_id
in: path
required: true
schema:
type: string
responses:
'200':
description: "Entry transferred status toggled successfully"
'400':
description: "Toggling transferred status failed"
/api/entries/accept/{value}:
get:
summary: "Set accept entries status"
parameters:
- name: value
in: path
required: true
schema:
type: string
responses:
'200':
description: "Accept entries status set successfully"
'400':
description: "Setting accept entries status failed"
/api/entries/accept:
get:
summary: "Get accept entries status"
responses:
'200':
description: "Accept entries status retrieved successfully"
/api/played/clear:
get:
summary: "Clear played songs"
responses:
'200':
description: "Played songs cleared successfully"
'400':
description: "Clearing played songs failed"
/api/entries/delete_all:
get:
summary: "Delete all entries"
responses:
'200':
description: "All entries deleted successfully"
'400':
description: "Deleting all entries failed"
/api/events/current:
get:
summary: "Get the current event"
responses:
'200':
description: "Current event retrieved successfully"
security:
- BasicAuth: []
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
BasicAuth:
type: http
scheme: basic

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

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

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).

View File

@ -13,11 +13,12 @@
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"preserveSymlinks": true,
"outputPath": "dist/ng-karaoqueue",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"tsConfig": "src/tsconfig.json",
"aot": false,
"assets": [
"src/favicon.ico",
@ -32,7 +33,13 @@
"src/custom-theme.scss",
"src/styles.scss"
],
"scripts": []
"scripts": [],
"vendorChunk": true,
"extractLicenses": false,
"buildOptimizer": false,
"sourceMap": true,
"optimization": false,
"namedChunks": true
},
"configurations": {
"production": {
@ -45,7 +52,6 @@
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
@ -64,7 +70,8 @@
}
]
}
}
},
"defaultConfiguration": ""
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
@ -106,19 +113,6 @@
"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": {
@ -134,10 +128,12 @@
}
}
},
"defaultProject": "ng-karaoqueue",
"schematics": {
"@schematics/angular:component": {
"styleext": "scss"
"style": "scss"
}
},
"cli": {
"analytics": false
}
}

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'.

View File

@ -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 } }));
}
};

View File

@ -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));
});
});

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>;
}
}

View File

@ -1,13 +0,0 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"module": "commonjs",
"target": "es5",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}

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

View File

@ -12,43 +12,40 @@
},
"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",
"@angular/animations": "^16.2.7",
"@angular/cdk": "^16.2.6",
"@angular/common": "^16.2.7",
"@angular/compiler": "^16.2.7",
"@angular/core": "^16.2.7",
"@angular/forms": "^16.2.7",
"@angular/material": "^16.2.6",
"@angular/platform-browser": "^16.2.7",
"@angular/platform-browser-dynamic": "^16.2.7",
"@angular/router": "^16.2.7",
"@mdi/angular-material": "^7.2.96",
"hammerjs": "^2.0.8",
"runtime-config-loader": "^2.0.0",
"rxjs": "~6.5.4",
"tslib": "^1.11.1",
"zone.js": "~0.10.2"
"rxjs": "~7.8.1",
"tslib": "^2.6.2",
"zone.js": "~0.14.0"
},
"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"
"@angular-devkit/build-angular": "^16.2.4",
"@angular/cli": "^16.2.4",
"@angular/compiler-cli": "^16.2.7",
"@angular/language-service": "^16.2.7",
"@types/jasmine": "~4.3.6",
"@types/jasminewd2": "~2.0.11",
"@types/node": "~20.7.2",
"jasmine-core": "~5.1.1",
"jasmine-spec-reporter": "~7.0.0",
"karma": "~6.4.2",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage-istanbul-reporter": "~3.0.3",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "^2.1.0",
"protractor": "~7.0.0",
"ts-node": "~10.9.1",
"tslint": "~6.1.0",
"typescript": "~5.2.2"
}
}
}

View File

@ -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!');
});
});

View File

@ -1,5 +1,5 @@
import { Component, OnInit } from '@angular/core';
import { RuntimeConfigLoaderService } from 'runtime-config-loader';
@Component({
selector: 'app-root',
@ -8,9 +8,9 @@ import { RuntimeConfigLoaderService } from 'runtime-config-loader';
})
export class AppComponent implements OnInit{
constructor(private configSvc: RuntimeConfigLoaderService) {}
constructor() {}
ngOnInit(): void {
console.log("API at ",this.configSvc.getConfigObjectKey("api"));
}
title = 'KaraoQueue';
}

View File

@ -4,39 +4,39 @@ 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 { MatLegacyAutocompleteModule as MatAutocompleteModule } from '@angular/material/legacy-autocomplete';
import { MatBadgeModule } from '@angular/material/badge';
import { MatBottomSheetModule } from '@angular/material/bottom-sheet';
import { MatButtonModule } from '@angular/material/button';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-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 { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox';
import { MatLegacyChipsModule as MatChipsModule } from '@angular/material/legacy-chips';
import { MatStepperModule } from '@angular/material/stepper';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatDialogModule } from '@angular/material/dialog';
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-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 { MatLegacyInputModule as MatInputModule } from '@angular/material/legacy-input';
import { MatLegacyListModule as MatListModule } from '@angular/material/legacy-list';
import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-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 { MatLegacyPaginatorModule as MatPaginatorModule } from '@angular/material/legacy-paginator';
import { MatLegacyProgressBarModule as MatProgressBarModule } from '@angular/material/legacy-progress-bar';
import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner';
import { MatLegacyRadioModule as MatRadioModule } from '@angular/material/legacy-radio';
import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-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 { MatLegacySliderModule as MatSliderModule } from '@angular/material/legacy-slider';
import { MatLegacySlideToggleModule as MatSlideToggleModule } from '@angular/material/legacy-slide-toggle';
import { MatLegacySnackBarModule as MatSnackBarModule } from '@angular/material/legacy-snack-bar';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table';
import { MatLegacyTabsModule as MatTabsModule } from '@angular/material/legacy-tabs';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip';
import { MatTreeModule } from '@angular/material/tree';
import { HttpClientModule } from '@angular/common/http';
@ -46,14 +46,14 @@ 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';
import { SongChipComponent } from './song-chip/song-chip.component';
@NgModule({
declarations: [
AppComponent,
EntryListingComponent,
SongSearchComponent
SongSearchComponent,
SongChipComponent
],
imports: [
BrowserModule,
@ -95,7 +95,6 @@ import { RuntimeConfigLoaderModule } from 'runtime-config-loader';
MatTooltipModule,
MatTreeModule,
HttpClientModule,
RuntimeConfigLoaderModule
],
providers: [],
bootstrap: [AppComponent]

View File

@ -1,6 +1,6 @@
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';

View File

@ -11,6 +11,6 @@ export class Genre {
}
id: number;
id: number;
_name: string;
}

View File

@ -4,23 +4,23 @@ 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>) {
constructor(title: string, artist: Artist, karafun_id: number, duo: boolean, explicit: boolean, id: number, styles: Array<String>, languages: Array<String>) {
this.title=title;
this.artist=artist;
this.karafun_id=karafun_id;
this.duet=duet;
this.duo=duo;
this.explicit=explicit;
this.id=id;
this.genres=genres;
this.styles=styles;
this.languages=languages;
}
title: string;
artist: Artist;
karafun_id: number;
duet: boolean;
duo: boolean;
explicit: boolean;
id: number;
genres: Array<String>;
styles: Array<String>;
languages: Array<String>;
}

View File

@ -0,0 +1,36 @@
<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.duo==1" [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==1" [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 style of song.styles">
<mat-chip>{{style}}</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>

View File

@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SongChipComponent } from './song-chip.component';
describe('SongChipComponent', () => {
let component: SongChipComponent;
let fixture: ComponentFixture<SongChipComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [SongChipComponent]
});
fixture = TestBed.createComponent(SongChipComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,10 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-song-chip',
templateUrl: './song-chip.component.html',
styleUrls: ['./song-chip.component.scss']
})
export class SongChipComponent {
}

View File

@ -8,41 +8,6 @@
</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>

View File

@ -4,8 +4,8 @@ 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';
import { ConnectableObservable, Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
@ -15,9 +15,9 @@ export class SongServiceService {
private api: string;
constructor(
private http: HttpClient,
private configSvc: RuntimeConfigLoaderService
) {
this.api=configSvc.getConfigObjectKey("api");
// TODO: get api from config
this.api= "http://localhost:5000/api";
}
searchSongByText(text: string): Observable<Array<Song>> {
@ -25,10 +25,19 @@ export class SongServiceService {
let out = new Array<Song>();
this.http.get(this.api +"/songs/compl?search="+text).subscribe((data: Observable<JSON>) => {
this.http.get(this.api +"/songs/search?q="+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"]));
let styles = new Array<String>();
let languages = new Array<String>();
for (let style of element["styles"].split(",")) {
styles.push(style);
}
for (let language of element["languages"].split(",")) {
languages.push(language);
}
out.push(new Song(element["title"],element["artist"],element["karafun_id"],element["duo"],element["explicit"],element["_id"],styles,languages));
});
console.log(out);
});
const observable = new Observable<Array<Song>>( subscriber => {

View File

@ -3,7 +3,7 @@
* https://materialtheme.arcsine.dev
*/
@import '~@angular/material/theming';
@import '@angular/material/theming';
// Include the common styles for Angular Material. We include this here so that you only
// have to load a single css file for Angular Material in your app.
@ -142,7 +142,7 @@ $mat-dark-theme-background: (
);
// Compute font config
@include mat-core($fontConfig);
@include mat-core();
// Theme Config

View File

@ -1,3 +0,0 @@
export const environment = {
production: true
};

View File

@ -1,16 +0,0 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 948 B

View File

@ -1,13 +1,4 @@
import 'hammerjs';
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
platformBrowserDynamic().bootstrapModule(AppModule);

View File

@ -1,3 +1,11 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
@ -8,8 +16,8 @@
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
* automatically update themselves. This includes recent versions of Safari, Chrome (including
* Opera), Edge on the desktop, and iOS and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
@ -18,16 +26,6 @@
* BROWSER POLYFILLS
*/
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
*/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
@ -35,7 +33,7 @@
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags.ts';
* import './zone-flags';
*
* The flags allowed in zone-flags.ts are listed here.
*
@ -55,9 +53,9 @@
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
import 'zone.js'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/
*/

View File

@ -1,20 +1,34 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone-testing';
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: any;
declare const require: {
context(path: string, deep?: boolean, filter?: RegExp): {
<T>(id: string): T;
keys(): string[];
};
};
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
platformBrowserDynamicTesting(),
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);
context.keys().forEach(context);

View File

@ -0,0 +1,28 @@
{
"compilerOptions": {
"baseUrl": "",
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"lib": [
"es6",
"dom"
],
"mapRoot": "./",
"module": "ES2022",
"moduleResolution": "node",
"outDir": "../dist/out-tsc",
"sourceMap": true,
"target": "ES2022",
"skipLibCheck": true,
"typeRoots": [
"../node_modules/@types"
],
"useDefineForClassFields": false
},
"include": [
"main.ts",
"polyfills.ts",
"src/**/*.d.ts"
]
}

View File

@ -1,3 +1,4 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
@ -5,14 +6,11 @@
"types": []
},
"files": [
"src/app/app.module.ts",
"src/main.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.ts"
],
"exclude": [
"src/test.ts",
"src/**/*.spec.ts"
"src/**/*.d.ts"
]
}
}

View File

@ -1,27 +1,17 @@
/*
This is a "Solution Style" tsconfig.json file, and is used by editors and TypeScripts language server to improve development experience.
It is not intended to be used to perform a compilation.
To learn more about this file see: https://angular.io/config/solution-tsconfig.
*/
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"module": "esnext",
"moduleResolution": "node",
"importHelpers": true,
"target": "es2015",
"typeRoots": [
"node_modules/@types"
"files": [],
"references": [
{
"path": "./src/tsconfig.json"
}
],
"lib": [
"es2018",
"dom"
]
},
"angularCompilerOptions": {
"fullTemplateTypeCheck": true,
"strictInjectionParameters": true,
"disableTypeScriptVersionCheck": true
}
}
"compilerOptions": {
"target": "ES2022",
}
}

View File

@ -1,10 +1,10 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine",
"node"
"jasmine"
]
},
"files": [

View File

@ -1,91 +0,0 @@
{
"extends": "tslint:recommended",
"rules": {
"array-type": false,
"arrow-parens": false,
"deprecation": {
"severity": "warning"
},
"component-class-suffix": true,
"contextual-lifecycle": true,
"directive-class-suffix": true,
"directive-selector": [
true,
"attribute",
"app",
"camelCase"
],
"component-selector": [
true,
"element",
"app",
"kebab-case"
],
"import-blacklist": [
true,
"rxjs/Rx"
],
"interface-name": false,
"max-classes-per-file": false,
"max-line-length": [
true,
140
],
"member-access": false,
"member-ordering": [
true,
{
"order": [
"static-field",
"instance-field",
"static-method",
"instance-method"
]
}
],
"no-consecutive-blank-lines": false,
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-empty": false,
"no-inferrable-types": [
true,
"ignore-params"
],
"no-non-null-assertion": true,
"no-redundant-jsdoc": true,
"no-switch-case-fall-through": true,
"no-var-requires": false,
"object-literal-key-quotes": [
true,
"as-needed"
],
"object-literal-sort-keys": false,
"ordered-imports": false,
"quotemark": [
true,
"single"
],
"trailing-comma": false,
"no-conflicting-lifecycle": true,
"no-host-metadata-property": true,
"no-input-rename": true,
"no-inputs-metadata-property": true,
"no-output-native": true,
"no-output-on-prefix": true,
"no-output-rename": true,
"no-outputs-metadata-property": true,
"template-banana-in-box": true,
"template-no-negated-async": true,
"use-lifecycle-interface": true,
"use-pipe-transform-interface": true
},
"rulesDirectory": [
"codelyzer"
]
}

141
package-lock.json generated Normal file
View File

@ -0,0 +1,141 @@
{
"name": "karaoqueue",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"runtime-config-loader": "^5.0.2"
}
},
"node_modules/@angular/common": {
"version": "16.2.7",
"resolved": "https://registry.npmjs.org/@angular/common/-/common-16.2.7.tgz",
"integrity": "sha512-vcKbbtDXNmJ8dj1GF52saJRT5U3P+phnIwnv+hQ2c+VVj/S2alWlBkT12iM+KlvnWdxsa0q4yW0G4WvpPJPaMQ==",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
"engines": {
"node": "^16.14.0 || >=18.10.0"
},
"peerDependencies": {
"@angular/core": "16.2.7",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/compiler": {
"version": "16.2.7",
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-16.2.7.tgz",
"integrity": "sha512-Sp+QjHFYjBMhjag/YbIV5skqr/UrpBjCPo1WFBBhj5DKkvgWC7T00yYJn+aBj0DU5ZuMmO/P8Vb7bRIHIRNL4w==",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
"engines": {
"node": "^16.14.0 || >=18.10.0"
},
"peerDependencies": {
"@angular/core": "16.2.7"
},
"peerDependenciesMeta": {
"@angular/core": {
"optional": true
}
}
},
"node_modules/@angular/core": {
"version": "16.2.7",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-16.2.7.tgz",
"integrity": "sha512-JQOxo+Ja9ThQjUa4vdOMLZfIK2dhR3cnPbqB1tV2WuTmIv49QASbFHsae8zZsS4Au5/TafBaW3KkK9aRU8G5gg==",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
"engines": {
"node": "^16.14.0 || >=18.10.0"
},
"peerDependencies": {
"rxjs": "^6.5.3 || ^7.4.0",
"zone.js": "~0.13.0"
}
},
"node_modules/@angular/platform-browser": {
"version": "16.2.7",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-16.2.7.tgz",
"integrity": "sha512-yQ/4FB33Jc1Xs+slWfddZpbKdkCHdhCh39Mfjxa1wTen6YJZKmvjBbMNCkvnvNbLqc2IFWRwTQdG8s0n1jfl3A==",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
"engines": {
"node": "^16.14.0 || >=18.10.0"
},
"peerDependencies": {
"@angular/animations": "16.2.7",
"@angular/common": "16.2.7",
"@angular/core": "16.2.7"
},
"peerDependenciesMeta": {
"@angular/animations": {
"optional": true
}
}
},
"node_modules/@angular/platform-browser-dynamic": {
"version": "16.2.7",
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-16.2.7.tgz",
"integrity": "sha512-raeuYEQfByHByLnA5YRR7fYD/5u6hMjONH77p08IjmtdmLb0XYP18l/C4YqsIOQG6kZLNCVWknEHZu3kuvAwtQ==",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
"engines": {
"node": "^16.14.0 || >=18.10.0"
},
"peerDependencies": {
"@angular/common": "16.2.7",
"@angular/compiler": "16.2.7",
"@angular/core": "16.2.7",
"@angular/platform-browser": "16.2.7"
}
},
"node_modules/runtime-config-loader": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/runtime-config-loader/-/runtime-config-loader-5.0.2.tgz",
"integrity": "sha512-6LnDfuV79wPuwykgxK0nDx/b28z6iXsKChlnc5OIr+iYPWNzgIe9MBaH7nNw9ACitWq4T1+Rkc+fvJeWNBBpug==",
"dependencies": {
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/common": ">=13.0.0",
"@angular/core": ">=13.0.0",
"@angular/platform-browser-dynamic": ">=13.0.0",
"rxjs": ">=6.6.0"
}
},
"node_modules/rxjs": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"peer": true,
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
},
"node_modules/zone.js": {
"version": "0.13.3",
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.13.3.tgz",
"integrity": "sha512-MKPbmZie6fASC/ps4dkmIhaT5eonHkEt6eAy80K42tAm0G2W+AahLJjbfi6X9NPdciOE9GRFTTM8u2IiF6O3ww==",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
}
}
}
}

5
package.json Normal file
View File

@ -0,0 +1,5 @@
{
"dependencies": {
"runtime-config-loader": "^5.0.2"
}
}