Initial commit

This commit is contained in:
Tobias Hopp 2023-08-03 19:20:27 +02:00
commit b581e9fbcf
23 changed files with 2832 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
/.idea
yarn-error.log
package-lock.json
/dist
/node_modules

30
package.json Normal file
View File

@ -0,0 +1,30 @@
{
"name": "livedj-app",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./dist/main.js"
},
"dependencies": {
"@types/cookie-parser": "^1.4.3",
"@types/debug": "^4.1.7",
"@types/express": "^4.17.14",
"@types/pug": "^2.0.6",
"@types/mongoose": "^5.11.97",
"@types/morgan": "^1.9.3",
"@types/node": "^18.11.9",
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"express": "~4.16.1",
"http-errors": "~1.6.3",
"morgan": "~1.9.1",
"pug": "2.0.0-beta11"
},
"devDependencies": {
"ts-loader": "^9.4.1",
"ts-node": "^10.9.1",
"typescript": "^5.1.6",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0"
}
}

BIN
public/images/not-playing.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 855 KiB

90
public/stylesheets/fs.css Executable file
View File

@ -0,0 +1,90 @@
root, *, body {
margin: 0;
cursor: none;
}
body {
text-align: center;
font-size: 3.5em;
font-weight: bold;
font-family: "Roboto Light", Ubuntu, sans-serif;
margin-top: 2%;
margin-left: 1%;
margin-right: 1%;
background-color: #0E2D2D;
color: white;
cursor: none;
}
.live-dj {
color: #3DE072;
margin-bottom: 4%;
}
.title {
margin-bottom: 4%;
color: #7E3E41;
}
.cover {
border-radius: 10px;
display: block;
width: 50%;
margin: 1% auto;
}
.current-playing-name {
font-size: 0.8em;
}
.current-playing-artist {
font-size: 0.4em;
}
.container {
display: grid;
grid-template-columns: 30% 25% 45%;
grid-template-rows: 100%;
text-align: center;
}
.queue {
cursor: none;
}
.now {
grid-row: span 1;
grid-column: span 1;
}
table {
font-family: arial, sans-serif;
border-collapse: collapse;
width: 90%;
margin-left: 5%;
background-color: #1D3636;
cursor: none;
}
td, th {
border: 1px solid #131A2A;
text-align: left;
padding: 8px;
}
tr th {
text-align: center;
}
tr:nth-child(even) {
background-color: #1D3636;
}

104
public/stylesheets/style.css Executable file
View File

@ -0,0 +1,104 @@
body {
background-color: #0E2D2D;
color: white;
font-family: "Roboto Light", Ubuntu, sans-serif;
overflow-x: hidden;
}
button {
font-size: 1.2em;
border-radius: 10px;
background-color: #42A4A4;
border: 1px solid #423434;
padding: 5px;
}
input {
font-size: 1.2em;
padding: 3px;
}
table {
font-family: arial, sans-serif;
border-collapse: collapse;
width: 99.99%;
background-color: #1D3636;
margin-bottom: 8%;
overflow-x: hidden;
-webkit-user-select: none; /* Safari */
-ms-user-select: none; /* IE 10 and IE 11 */
user-select: none; /* Standard syntax */
}
td, th {
border: 1px solid #131A2A;
text-align: left;
padding: 8px;
}
tr th {
text-align: center;
}
tr:active {
background-color: #66A1A1 !important;
}
tr:nth-child(even) {
background-color: #1F4444;
}
/* The Modal (background) */
.modal {
display: none; /* Hidden by default */
position: fixed; /* Stay in place */
z-index: 1; /* Sit on top */
padding-top: 100px; /* Location of the box */
left: 0;
top: 0;
width: 100%; /* Full width */
height: 100%; /* Full height */
overflow: auto; /* Enable scroll if needed */
background-color: rgb(0,0,0); /* Fallback color */
background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
}
/* Modal Content */
.modal-content {
background-color: #fefefe;
margin: auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
color: black;
}
/* The Close Button */
.close {
color: #aaaaaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close:hover,
.close:focus {
color: #000;
text-decoration: none;
cursor: pointer;
}
.footer {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
background-color: #0E2D2D;
color: white;
text-align: center;
}

9
routes/index.js Normal file
View File

@ -0,0 +1,9 @@
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
module.exports = router;

9
routes/users.js Normal file
View File

@ -0,0 +1,9 @@
var express = require('express');
var router = express.Router();
/* GET users listing. */
router.get('/', function(req, res, next) {
res.send('respond with a resource');
});
module.exports = router;

91
src/App.ts Normal file
View File

@ -0,0 +1,91 @@
import express from 'express';
import path from "path";
import morgan from "morgan";
import cookieParser from "cookie-parser";
import debug from "debug";
import * as http from "http";
import {AddressInfo} from "net";
export class App {
get app(): express.Application {
return this._app;
}
private readonly _app: express.Application;
private readonly _server;
static port = 3000;
private log = debug("livedj:app");
constructor() {
this._app = express();
this._server = http.createServer(this._app);
this._app.set('views', path.join(__dirname, '../views'));
this._app.set('view engine', 'pug');
this._app.use(morgan('dev'));
this._app.use(express.json());
this._app.use(express.urlencoded({extended: false}));
this._app.use(cookieParser());
this._app.use(express.static(path.join(__dirname, "../public")));
this._app.use('/web.js', express.static(path.join(__dirname, "../dist/web.bundle.js")));
this._app.use( (req, res, next) => {
next();
} )
this._app.use((err, req, res, next) => {
res.locals.message = err.message;
res.locals.error = err;
res.status(err.status || 500);
res.render('error');
this.log("Error " + err);
});
this.loadRoutes();
}
public loadRoutes( ) : void
{
this._app.use( "/", require("./routes/index") );
this._app.use( "/api/", require("./routes/api") );
}
public listen(): Promise<void> {
return new Promise((resolve, reject) => {
this._server.on('error', (error) => {
if (error.message != 'listen') {
reject();
return;
}
let bind = 'Port ' + App.port;
// handle specific listen errors with friendly messages
switch (error.name) {
case 'EACCES':
reject(bind + ' requires elevated privileges');
break;
case 'EADDRINUSE':
reject(bind + ' is already in use');
break;
default:
reject();
}
});
this._server.on('listening', () => {
let addr = this._server.address() as AddressInfo;
this.log("Listening on " + addr.port);
resolve();
})
this._server.listen(App.port);
});
}
}

46
src/main.ts Normal file
View File

@ -0,0 +1,46 @@
import {App} from "./App";
import debug from "debug";
import path from "path";
const log = debug("itender:server");
const app = new App();
global.appRoot = path.resolve(__dirname);
process.on("uncaughtException", (error) => {
});
process.on("unhandledRejection", (reason, promise) => {
});
(async () => {
try {
log("Starting...");
//await Database.connect();
await app.listen();
} catch (e) {
console.error("---- ERROR ----");
console.error(e);
process.exit(-1);
}
})();
/**
* tst
*/
function init(): Promise<void> {
return new Promise(async resolve => {
log("Initializing...");
resolve();
});
}

14
src/routes/api.ts Normal file
View File

@ -0,0 +1,14 @@
import express from "express";
const router = express.Router();
/* GET home page. */
router.get('/', function (req, res, next) {
res.render('index');
});
router.get('/status', (req, res) => {
res.status(200).json({status: "ok", code: 200});
})
module.exports = router;

14
src/routes/index.ts Normal file
View File

@ -0,0 +1,14 @@
import express from "express";
const router = express.Router();
/* GET home page. */
router.get('/', function (req, res, next) {
res.render('index');
});
router.get('/status', (req, res) => {
res.status(200).json({status: "ok", code: 200});
})
module.exports = router;

51
src/web/fs.js Executable file
View File

@ -0,0 +1,51 @@
let queue = document.getElementById("queue-items");
let current_name = document.getElementById("current-name");
let current_artist = document.getElementById("current-artist");
let current_cover = document.getElementById("current-cover");
function refresh() {
httpGetAsync("/api/current-queue", (response, status) => {
if(status !== 200)
return alert("Anfrage fehlgeschlagen!");
queue.innerHTML = "";
current_name.innerText = String(response.current.name);
current_artist.innerText = String(response.current.artist);
current_cover.src = response.current.cover;
let i = 1;
for( let e of response.next )
{
let tr = document.createElement("tr");
let nr = document.createElement("td");
nr.innerText = String(i);
let cover = document.createElement("td");
let cover_img = document.createElement("img");
cover_img.src = e.cover;
cover_img.classList.add("cover");
cover_img.style.width = "50px";
cover_img.style.height = "50px";
cover.append(cover_img);
let artist = document.createElement("td");
artist.innerText = e.artist;
let name = document.createElement("td");
name.innerText = e.name;
tr.append(nr,cover,artist, name);
queue.append(tr);
if( i === 9 )
return;
i++;
}
});
}
refresh();
setInterval(refresh, 1000*10);

0
src/web/main.ts Normal file
View File

87
src/web/push.js Executable file
View File

@ -0,0 +1,87 @@
const search_box = document.getElementById("search")
const results = document.getElementById("results")
const result_count = document.getElementById("result_count")
const search_btn = document.getElementById("search_btn");
function search()
{
if( search_box.value.trim().length === 0)
{
return openModal("Huh?", "Gebe bitte einen Künstler, Titel oder ein Album ein");
}
search_btn.disabled = true;
httpGetAsync("/api/search-song/" + search_box.value, (response, status) => {
search_btn.disabled = false;
if ( status === 429 )
return openModal("Nicht so schnell!", "Lass Spotify etwas Zeit zwischen deinen Suchanfragen.");
else if(status !== 200)
return openModal("Oops!", "Etwas ist schiefgelaufen.<br>Versuche es später erneut oder kontaktiere die Verwaltung.");
results.innerHTML = "";
let i = 1;
for( let e of response )
{
let tr = document.createElement("tr");
tr.onclick = () => add(e.uri);
let cover = document.createElement("td");
let cover_img = document.createElement("img");
cover_img.src = e.cover;
cover_img.classList.add("cover");
cover_img.style.width = "30px";
cover_img.style.height = "30px";
cover.append(cover_img);
let artist = document.createElement("td");
artist.innerText = e.artist;
let name = document.createElement("td");
name.innerText = e.name;
tr.append(cover,artist, name);
results.append(tr);
i++;
}
result_count.innerText = i + " Ergebnisse";
});
}
search_box.addEventListener("keyup", function(event) {
if (event.key === "Enter") {
search();
}
});
function add(uri)
{
if( Date.now() < last_add+1000) // Anti double click
return;
httpPostAsync("/api/push-queue", {uri: uri}, (r, status) => {
if( status === 429 )
return openModal("Nicht so schnell 👋🛑", "Lass den anderen auch etwas Zeit ⏲️<br>Du darfst erneut in " + Math.round(r.retry_in) + "s");
else if ( status === 404 )
return openModal("The Party is over!", "Gerade wird keine Musik gespielt, weswegen auch keine Warteschlange existiert :(");
else if(status === 200)
{
results.innerText = "";
result_count.innerText = "0 Ergebnisse";
search_box.value = "";
last_add = Date.now();
return openModal("Hinzugefügt! ", "Dein Titel wurde der Warteschlange hinzugefügt");
}
else
{
return openModal("Oops!", "Etwas ist schiefgelaufen.<br>Versuche es später erneut oder kontaktiere die Verwaltung.");
}
})
}

64
src/web/request.js Executable file
View File

@ -0,0 +1,64 @@
function httpGetAsync(url, callback) {
const xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function () {
if (xmlHttp.readyState === 4)
{
try {
callback(JSON.parse(xmlHttp.responseText), xmlHttp.status);
} catch( e )
{
callback({}, 500);
}
}
}
xmlHttp.open("GET", url, true); // true for asynchronous
xmlHttp.send(null);
}
function httpPostAsync(url, data, callback) {
const xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function () {
if (xmlHttp.readyState === 4)
try {
callback(JSON.parse(xmlHttp.responseText), xmlHttp.status);
} catch( e )
{
callback({}, 500);
}
}
xmlHttp.open("POST", url, true); // true for asynchronous
xmlHttp.setRequestHeader("Content-Type", "application/json");
xmlHttp.send(JSON.stringify(data));
}
const modal = document.getElementById("myModal");
const modal_title = document.getElementById("modal-title");
const modal_text = document.getElementById("modal-text");
let last_add = 0;
// Get the <span> element that closes the modal
const modal_span = document.getElementsByClassName("close")[0];
// When the user clicks on <span> (x), close the modal
modal_span.onclick = function() {
modal.style.display = "none";
}
// When the user clicks anywhere outside of the modal, close it
window.onclick = function(event) {
if (event.target === modal) {
modal.style.display = "none";
}
}
function openModal(title,text)
{
modal_title.innerText = title;
modal_text.innerHTML = text;
modal.style.display = "block"
}

30
tsconfig.json Normal file
View File

@ -0,0 +1,30 @@
{
"compilerOptions": {
"pretty": true,
"noImplicitAny": false,
"sourceMap": true,
"outDir": "dist",
"target": "ES6",
"module": "CommonJS",
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"removeComments": true,
"suppressImplicitAnyIndexErrors": false,
"resolveJsonModule": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": false,
"esModuleInterop": true,
"strict": false,
"typeRoots": [
"node_modules/@types"
]
},
"include": [
"./src/"
],
"exclude": [
"node_modules"
]
}

7
views/auth-post.pug Normal file
View File

@ -0,0 +1,7 @@
html
head
title Auth Pusher
body
h1 Sending...
script(src="/javascripts/request.js")
script(src="/javascripts/push.js")

6
views/error.pug Normal file
View File

@ -0,0 +1,6 @@
extends layout
block content
h1= message
h2= error.status
pre #{error.stack}

29
views/fullscreen.pug Executable file
View File

@ -0,0 +1,29 @@
html
head
title Currently Playing and Queue!
meta(charset="UTF-8")
meta(name="viewport" content="width=device-width, initial-scale=1")
link(rel="stylesheet" href="/stylesheets/fs.css")
body
h1.live-dj #Live DJ
div.container
div.now
h4.title Spielt aktuell
img.cover#current-cover(style="width:350px; height:350px" src="https://i.scdn.co/image/ab67616d00001e020500a6a79bd0fd15046ae0c1")
p.current-playing-name#current-name Happy Birthday
p.current-playing-artist#current-artist Nika
div.now
h4.title Titel hinzufügen?
img(src="https://api.qrserver.com/v1/create-qr-code/?size=350x350&ecc=M&color=90-195-195&bgcolor=14-45-45&data=https://iif.li/party/" style="width: 350px; height: 350px; border: 0; border-radius: 3px;")
div.now
h4.title Warteschlange
table.queue
thead
tr
th Nr.
th Cover
th Künstler
th Song
tbody#queue-items
script(src="/javascripts/request.js")
script(src="/javascripts/fs.js")

30
views/index.pug Normal file
View File

@ -0,0 +1,30 @@
extends layout
block content
div#myModal.modal
div.modal-content
span.close &times
h1#modal-title Title
p#modal-text Text
h1 #Live DJ — Queue
span(style="color: #54949D;") Suche zuerst nach einem Song, Künstler oder Album und klicke danach einfach auf ein Ergebnis
hr
div(style="width:100%")
input#search(type="text" placeholder="In Spotify suchen" style="width: 65%; margin-right: 5%")
button#search_btn(style="width: 25%" onclick="search()") Suchen
br
br
span(style="color: grey")#result_count 0 Ergebnisse
table.search
thead
tr
th Cover
th Künstler
th Song
tbody#results
br
footer.footer
p Programmed with ❤️ by git/Tobstr02

11
views/layout.pug Executable file
View File

@ -0,0 +1,11 @@
doctype html
html
head
title= title
link(rel='stylesheet', href='/stylesheets/style.css')
meta(charset="UTF-8")
meta(name="viewport" content="width=device-width, initial-scale=1")
body
block content
script(src="/web.js")

27
webpack.config.js Normal file
View File

@ -0,0 +1,27 @@
const path = require( "path" );
module.exports = {
mode: "development",
devtool: "inline-source-map",
entry: {
web: "./src/web/main.ts",
},
module: {
rules: [
{
test: /\.tsx?$/,
use: "ts-loader",
//exclude: /node_modules/,
},
],
},
resolve: {
extensions: [".tsx", ".ts", ".js"],
},
output: {
filename: "[name].bundle.js",
path: path.resolve( __dirname, "dist" ),
},
};

2078
yarn.lock Normal file

File diff suppressed because it is too large Load Diff