Compare commits

..

8 Commits

Author SHA1 Message Date
576b6f9490 Fix some errors, style updates 2025-11-22 22:37:36 +01:00
e0dcfa6964 Finish bar utility 2025-11-22 17:43:25 +01:00
Janis Hutz
69d2db8c37 Update BarView.vue
Fix bar config caching
2025-11-22 12:40:11 +00:00
9a347c9206 Bar utility: Fix error 2025-11-21 17:20:54 +01:00
60ea4669ab Bar utility: some updates 2025-11-21 17:18:16 +01:00
669cc620bf Fixes for uncommon songs with that are not on apple music 2025-11-20 17:15:39 +01:00
64d086dec4 Format 2025-11-20 16:55:41 +01:00
1d714da494 Player: Improve custom song loading song data resolution 2025-11-20 16:38:31 +01:00
14 changed files with 658 additions and 146 deletions

View File

@@ -1,19 +1,193 @@
{
"ages": {
"below": "red",
"16-18": "",
"18+": ""
"below": "Orange",
"16-18": "Yellow",
"18+": "Turquoise"
},
"offering": {
"test": {
"name": "Test drink",
"price": 700,
"id": "test"
"big-bar": {
"offering": {
"softdrinks": {
"name": "Softdrinks",
"price": 400,
"id": "softdrinks"
},
"energy": {
"name": "Energy",
"price": 400,
"id": "energy"
},
"mate": {
"name": "Mate",
"price": 500,
"id": "mate",
"depot": 200
},
"sparkly-water": {
"name": "Mineralwasser mit",
"price": 300,
"id": "sparkly-water",
"showLine": true
},
"rose": {
"name": "Rosé",
"price": 1500,
"id": "rose",
"depot": 200
},
"red": {
"name": "Rotwein",
"price": 2000,
"id": "red",
"depot": 200
},
"wine-glasses": {
"name": "Weingläser",
"price": 0,
"id": "wine-glasses",
"depot": 200
},
"appenzeller": {
"name": "Appenzeller Vollmond",
"price": 500,
"id": "appenzeller",
"depot": 200
},
"feldschloesschen": {
"name": "Feldschlösschen",
"price": 500,
"id": "feldschloesschen",
"depot": 200
},
"sommersby": {
"name": "Sommersby",
"price": 500,
"id": "sommersby",
"depot": 200
},
"jever-fun": {
"name": "Jever Fun",
"price": 400,
"id": "jever-fun",
"depot": 200
},
"trojka-ice": {
"name": "Trojka Ice",
"price": 600,
"id": "trojka-ice",
"depot": 200,
"showLine": true
},
"vodka-red-energy": {
"name": "Vodka Rot Energy",
"price": 800,
"id": "vodka-red-energy"
},
"vodka-green-citro": {
"name": "Vodka Grün Citro",
"price": 800,
"id": "vodka-green-citro"
},
"vodka-white-energy": {
"name": "Vodka White Energy",
"price": 900,
"id": "vodka-white-energy"
},
"gin-tonic": {
"name": "Gin Tonic",
"price": 900,
"id": "gin-tonic"
},
"rum-cola": {
"name": "Rum Cola",
"price": 900,
"id": "rum-cola"
},
"whiskey-cola": {
"name": "Whiskey Cola",
"price": 900,
"id": "whiskey-cola"
},
"mate-mit-schuss": {
"name": "Mate mit Schuss",
"price": 1200,
"id": "mate-mit-schuss",
"depot": 200,
"showLine": true
},
"poseidon": {
"name": "Poseidon",
"price": 900,
"id": "poseidon"
},
"arielle": {
"name": "Arielle",
"price": 900,
"id": "arielle"
},
"pearl-driver": {
"name": "Pearl Driver",
"price": 400,
"id": "pearl-driver"
}
},
"name": "Poseidon's Quelle",
"id": "big-bar"
},
"test-2": {
"name": "Test drink 2",
"price": 500,
"id": "test-2"
"small-bar": {
"offering": {
"softdrinks": {
"name": "Softdrinks (Alle)",
"price": 300,
"id": "softdrinks",
"showLine": true
},
"appenzeller": {
"name": "Appenzeller Vollmond",
"price": 500,
"id": "appenzeller",
"depot": 200
},
"feldschloesschen": {
"name": "Feldschlösschen",
"price": 500,
"id": "feldschloesschen",
"depot": 200
},
"sommersby": {
"name": "Sommersby",
"price": 500,
"id": "sommersby",
"depot": 200
},
"jever-fun": {
"name": "Jever Fun",
"price": 400,
"id": "jever-fun",
"depot": 200,
"showLine": true
},
"rose": {
"name": "Rosé",
"price": 1500,
"id": "rose",
"depot": 200
},
"red": {
"name": "Rotwein",
"price": 2000,
"id": "red",
"depot": 200
},
"wine-glasses": {
"name": "Weingläser",
"price": 0,
"id": "wine-glasses",
"depot": 200
}
},
"name": "Seepferdchenbar",
"id": "small-bar"
}
}
}

View File

@@ -431,23 +431,46 @@
parseBlob( blob )
.then( data => {
try {
player.findSongOnAppleMusic( data.common.title
?? songDetails.filename.split( '.' )[ 0 ] )
const searchTerm = data.common.title
? data.common.title + ( data.common.artist ? ' ' + data.common.artist : '' )
: songDetails.filename.split( '.' )[ 0 ].replace( '_', ' ' );
player.findSongOnAppleMusic( searchTerm )
.then( d => {
let url = d.data.results.songs.data[ 0 ].attributes.artwork.url;
if ( d.data.results.songs ) {
let url = d.data.results.songs.data[ 0 ].attributes.artwork.url;
url = url.replace( '{w}', String( d.data.results.songs.data[ 0 ].attributes.artwork.width ) );
url = url.replace( '{h}', String( d.data.results.songs.data[ 0 ].attributes.artwork.height ) );
const song: Song = {
'artist': d.data.results.songs.data[ 0 ].attributes.artistName,
'title': d.data.results.songs.data[ 0 ].attributes.name,
'duration': data.format.duration ?? ( d.data.results.songs.data[ 0 ].attributes.durationInMillis / 1000 ),
'id': songDetails.url,
'origin': 'disk',
'cover': url
};
console.debug(
'Result used for', searchTerm, 'is', d.data.results.songs.data[0]
);
resolve( song );
url = url.replace( '{w}', String( d.data.results.songs.data[ 0 ].attributes.artwork.width ) );
url = url.replace( '{h}', String( d.data.results.songs.data[ 0 ].attributes.artwork.height ) );
const song: Song = {
'artist': d.data.results.songs.data[ 0 ].attributes.artistName,
'title': d.data.results.songs.data[ 0 ].attributes.name,
'duration': data.format.duration ?? ( d.data.results.songs.data[ 0 ].attributes.durationInMillis / 1000 ),
'id': songDetails.url,
'origin': 'disk',
'cover': url
};
resolve( song );
} else {
const song: Song = {
'artist': data.common.artist ?? 'Unknown artist',
'title': data.common.title ?? 'Unknown song title',
'duration': data.format.duration ?? 1000,
'id': songDetails.url,
'origin': 'disk',
'cover': ''
};
console.warn( 'No results found for', searchTerm );
resolve( song );
}
} )
.catch( e => {
console.error( e );
@@ -560,7 +583,7 @@
prepNiceDurationTime( playingSong );
notificationHandler.emit( 'playlist-index-update', currentlyPlayingSongIndex.value );
notificationHandler.emit( 'playback-update', isPlaying.value );
notificationHandler.emit( 'playback-start-update', new Date().getTime() - pos.value * 1000 );
notificationHandler.emit( 'playback-start-update', new Date().getTime() - ( pos.value * 1000 ) );
hasStarted = true;
}, 2000 );
}
@@ -574,7 +597,7 @@
nicePlaybackPos.value = '0' + minuteCount + ':';
}
const secondCount = Math.floor( pos.value - minuteCount * 60 );
const secondCount = Math.floor( pos.value - ( minuteCount * 60 ) );
if ( ( '' + secondCount ).length === 1 ) {
nicePlaybackPos.value += '0' + secondCount;
@@ -591,7 +614,7 @@
niceDuration.value = '-0' + minuteCounts + ':';
}
const secondCounts = Math.floor( ( playingSong.duration - pos.value ) - minuteCounts * 60 );
const secondCounts = Math.floor( ( playingSong.duration - pos.value ) - ( minuteCounts * 60 ) );
if ( ( '' + secondCounts ).length === 1 ) {
niceDuration.value += '0' + secondCounts;

View File

@@ -1,88 +1,153 @@
<template>
<div>
<h1>Queue</h1>
<input type="file" multiple accept="audio/*" id="more-songs" class="small-buttons">
<button @click="addNewSongs()" class="small-buttons" title="Load selected files"><span class="material-symbols-outlined">upload</span></button>
<button @click="openSearch()" v-if="$props.isLoggedIntoAppleMusic" class="small-buttons" title="Search Apple Music for the song"><span class="material-symbols-outlined">search</span></button>
<button @click="clearPlaylist()" class="small-buttons" title="Clear the playlist"><span class="material-symbols-outlined">delete</span></button>
<button title="Transmit additional information" class="small-buttons" @click="sendAdditionalInfo()"><span class="material-symbols-outlined">send</span></button>
<p v-if="!hasSelectedSongs">Please select at least one song to proceed</p>
<div class="playlist-box" id="pl-box">
<input
id="more-songs"
type="file"
multiple
accept="audio/*"
class="small-buttons"
>
<button class="small-buttons" title="Load selected files" @click="addNewSongs()">
<span class="material-symbols-outlined">upload</span>
</button>
<button
v-if="$props.isLoggedIntoAppleMusic"
class="small-buttons"
title="Search Apple Music for the song"
@click="openSearch()"
>
<span class="material-symbols-outlined">search</span>
</button>
<button class="small-buttons" title="Clear the playlist" @click="clearPlaylist()">
<span class="material-symbols-outlined">delete</span>
</button>
<button title="Transmit additional information" class="small-buttons" @click="sendAdditionalInfo()">
<span class="material-symbols-outlined">send</span>
</button>
<p v-if="!hasSelectedSongs">
Please select at least one song to proceed
</p>
<div id="pl-box" class="playlist-box">
<!-- TODO: Allow editing additionalInfo. Think also how to make it persist over reloads... Export to JSON and then best-guess add them? Very easy for Apple Music 'cause ID, but how for local songs? Maybe using retrieved ID from Apple Music? -->
<!-- TODO: Handle long AppleMusic Playlists, as AppleMusic doesn't automatically load all songs of a playlist -->
<div class="song" v-for="song in computedPlaylist" v-bind:key="song.id"
:class="( song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) && isPlaying ? 'playing' : ' not-playing' )
+ ( ( !isPlaying && ( song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) ) ) ? ' active-song' : '' )">
<img :src="song.cover" alt="Song cover" class="song-cover">
<div
v-for="song in computedPlaylist"
:key="song.id"
class="song"
:class="( song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) && isPlaying ? 'playing' : ' not-playing' )
+ ( ( !isPlaying && ( song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) ) ) ? ' active-song' : '' )"
>
<img
v-if="song.cover"
:src="song.cover"
alt="Song cover"
class="song-cover"
>
<span v-else class="material-symbols-outlined song-cover">music_note</span>
<div v-if="song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) && $props.isPlaying" class="playing-symbols">
<div class="playing-symbols-wrapper">
<div class="playing-bar" id="bar-1"></div>
<div class="playing-bar" id="bar-2"></div>
<div class="playing-bar" id="bar-3"></div>
<div id="bar-1" class="playing-bar"></div>
<div id="bar-2" class="playing-bar"></div>
<div id="bar-3" class="playing-bar"></div>
</div>
</div>
<span class="material-symbols-outlined play-icon" @click="control( 'play' )" v-if="song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' )">play_arrow</span>
<span class="material-symbols-outlined play-icon" @click="play( song.id )" v-else>play_arrow</span>
<span v-if="song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' )" class="material-symbols-outlined play-icon" @click="control( 'play' )">play_arrow</span>
<span v-else class="material-symbols-outlined play-icon" @click="play( song.id )">play_arrow</span>
<span class="material-symbols-outlined pause-icon" @click="control( 'pause' )">pause</span>
<span class="material-symbols-outlined move-icon" @click="moveSong( song.id, 'up' )" title="Move song up" v-if="canBeMoved( 'up', song.id )">arrow_upward</span>
<span class="material-symbols-outlined move-icon" @click="moveSong( song.id, 'down' )" title="Move song down" v-if="canBeMoved( 'down', song.id )">arrow_downward</span>
<h3 class="song-title">{{ song.title }}</h3>
<span
v-if="canBeMoved( 'up', song.id )"
class="material-symbols-outlined move-icon"
title="Move song up"
@click="moveSong( song.id, 'up' )"
>arrow_upward</span>
<span
v-if="canBeMoved( 'down', song.id )"
class="material-symbols-outlined move-icon"
title="Move song down"
@click="moveSong( song.id, 'down' )"
>arrow_downward</span>
<h3 class="song-title">
{{ song.title }}
</h3>
<div>
<input type="text" placeholder="Additional information for remote display" title="Additional information for remote display" v-model="song.additionalInfo" @focusin="kbControl( 'on' )" @focusout="kbControl( 'off' )">
<p class="playing-in">{{ getTimeUntil( song ) }}</p>
<input
v-model="song.additionalInfo"
type="text"
placeholder="Additional information for remote display"
title="Additional information for remote display"
@focusin="kbControl( 'on' )"
@focusout="kbControl( 'off' )"
>
<p class="playing-in">
{{ getTimeUntil( song ) }}
</p>
</div>
<button @click="deleteSong( song.id )" class="small-buttons" title="Remove this song from the queue" v-if="canBeMoved( 'down', song.id ) || canBeMoved( 'up', song.id )"><span class="material-symbols-outlined">delete</span></button>
<button
v-if="canBeMoved( 'down', song.id ) || canBeMoved( 'up', song.id )"
class="small-buttons"
title="Remove this song from the queue"
@click="deleteSong( song.id )"
>
<span class="material-symbols-outlined">delete</span>
</button>
</div>
</div>
<searchView ref="search" @selected-song="( song ) => { addNewSongsAppleMusic( song ) }"></searchView>
<searchView ref="search" @selected-song="( song ) => { addNewSongsAppleMusic( song ) }" />
</div>
</template>
<script setup lang="ts">
// TODO: Add logout button
import type { AppleMusicSongData, ReadFile, Song } from '@/scripts/song';
import { computed, ref } from 'vue';
import type {
AppleMusicSongData, ReadFile, Song
} from '@/scripts/song';
import {
computed, ref
} from 'vue';
import searchView from './searchView.vue';
import { useUserStore } from '@/stores/userStore';
import {
useUserStore
} from '@/stores/userStore';
const userStore = useUserStore();
const search = ref( searchView );
const props = defineProps( {
const props = defineProps( {
'playlist': {
default: [],
required: true,
type: Array<Song>
'default': [],
'required': true,
'type': Array<Song>
},
'currentlyPlaying': {
default: 0,
required: true,
type: Number,
'default': 0,
'required': true,
'type': Number,
},
'isPlaying': {
default: true,
required: true,
type: Boolean,
'default': true,
'required': true,
'type': Boolean,
},
'pos': {
default: 0,
required: false,
type: Number,
'default': 0,
'required': false,
'type': Number,
},
'isLoggedIntoAppleMusic': {
default: false,
required: true,
type: Boolean,
'default': false,
'required': true,
'type': Boolean,
}
} );
const hasSelectedSongs = ref( true );
const computedPlaylist = computed( () => {
let pl: Song[] = [];
// ( document.getElementById( 'pl-box' ) as HTMLDivElement ).scrollTo( { behavior: 'smooth', top: 0 } );
for ( let i = props.currentlyPlaying; i < props.playlist.length; i++ ) {
pl.push( props.playlist[ i ] );
}
return pl;
} );
@@ -92,60 +157,66 @@
} else {
userStore.setKeyboardUsageStatus( true );
}
}
};
const openSearch = () => {
if ( search.value ) {
search.value.controlSearch( 'show' );
}
}
};
const canBeMoved = computed( () => {
return ( direction: movementDirection, songID: string ): boolean => {
let id = 0;
for ( let song in props.playlist ) {
if ( props.playlist[ song ].id === songID ) {
id = parseInt( song );
break;
}
}
if ( direction === 'up' ) {
if ( props.currentlyPlaying + 1 === id || props.currentlyPlaying === id ) {
return false;
}
return true;
} else {
if ( id === props.playlist.length - 1 || props.currentlyPlaying === id ) {
return false;
}
return true;
}
}
} )
};
} );
const getTimeUntil = computed( () => {
return ( song: Song ) => {
let timeRemaining = 0;
for ( let i = props.currentlyPlaying; i < Object.keys( props.playlist ).length; i++ ) {
if ( props.playlist[ i ] == song ) {
break;
}
timeRemaining += props.playlist[ i ].duration;
}
if ( props.isPlaying ) {
if ( timeRemaining === 0 ) {
return 'Currently playing';
} else {
return 'Playing in less than ' + Math.ceil( timeRemaining / 60 - props.pos / 60 ) + 'min';
return 'Playing in less than ' + Math.ceil( timeRemaining / 60 - props.pos / 60 ) + 'min';
}
} else {
if ( timeRemaining === 0 ) {
return 'Plays next';
} else {
return 'Playing less than ' + Math.ceil( timeRemaining / 60 - props.pos / 60 ) + 'min after starting to play';
return 'Playing less than ' + Math.ceil( timeRemaining / 60 - props.pos / 60 ) + 'min after starting to play';
}
}
}
};
} );
const deleteSong = ( songID: string ) => {
@@ -154,69 +225,92 @@
emits( 'delete-song', parseInt( song ) );
}
}
}
};
const clearPlaylist = () => {
emits( 'clear-playlist', '' );
}
};
const control = ( action: string ) => {
emits( 'control', action );
}
};
const play = ( song: string ) => {
emits( 'play-song', song );
}
};
const addNewSongs = () => {
const fileURLList: ReadFile[] = [];
const allFiles = ( document.getElementById( 'more-songs' ) as HTMLInputElement ).files ?? [];
if ( allFiles.length > 0 ) {
hasSelectedSongs.value = true;
for ( let file = 0; file < allFiles.length; file++ ) {
fileURLList.push( { 'url': URL.createObjectURL( allFiles[ file ] ), 'filename': allFiles[ file ].name } );
fileURLList.push( {
'url': URL.createObjectURL( allFiles[ file ] ),
'filename': allFiles[ file ].name
} );
}
emits( 'add-new-songs', fileURLList );
} else {
hasSelectedSongs.value = false;
}
}
};
const addNewSongsAppleMusic = ( songData: AppleMusicSongData ) => {
const song: Song = {
artist: songData.attributes.artistName,
cover: songData.attributes.artwork.url.replace( '{w}', String( songData.attributes.artwork.width ) ).replace( '{h}', String( songData.attributes.artwork.height ) ),
duration: songData.attributes.durationInMillis / 1000,
id: songData.id,
origin: 'apple-music',
title: songData.attributes.name
}
'artist': songData.attributes.artistName,
'cover': songData.attributes.artwork.url.replace( '{w}', String( songData.attributes.artwork.width ) ).replace( '{h}', String( songData.attributes.artwork.height ) ),
'duration': songData.attributes.durationInMillis / 1000,
'id': songData.id,
'origin': 'apple-music',
'title': songData.attributes.name
};
emits( 'add-new-songs-apple-music', song );
}
};
type movementDirection = 'up' | 'down';
const moveSong = ( songID: string, direction: movementDirection ) => {
let newSongPos = 0;
let hasFoundSongToMove = false;
for ( let el in props.playlist ) {
if ( props.playlist[ el ].id === songID ) {
const currPos = parseInt( el );
newSongPos = currPos + ( direction === 'up' ? -1 : 1 );
hasFoundSongToMove = true;
break;
}
}
if ( hasFoundSongToMove ) {
emits( 'playlist-reorder', { 'songID': songID, 'newPos': newSongPos } );
emits( 'playlist-reorder', {
'songID': songID,
'newPos': newSongPos
} );
}
}
};
const sendAdditionalInfo = () => {
emits( 'send-additional-info' );
}
};
const emits = defineEmits( [ 'play-song', 'control', 'playlist-reorder', 'add-new-songs', 'add-new-songs-apple-music', 'delete-song', 'clear-playlist', 'send-additional-info' ] );
const emits = defineEmits( [
'play-song',
'control',
'playlist-reorder',
'add-new-songs',
'add-new-songs-apple-music',
'delete-song',
'clear-playlist',
'send-additional-info'
] );
</script>
<style scoped>
@@ -249,6 +343,10 @@
font-size: 6rem;
}
.song img.song-cover {
font-size: unset;
}
.song-title {
margin-left: 10px;
margin-right: auto;
@@ -372,4 +470,4 @@
.small-buttons:hover .material-symbols-outlined {
transform: scale(1.1);
}
</style>
</style>

View File

@@ -8,7 +8,10 @@
<!-- TODO: Make prettier -->
</div>
<div v-else-if="!$props.isLoggedIn" class="not-logged-in">
<p>You are not logged into Apple Music. We therefore can't show you your playlists. <a href="" title="Refreshes the page, allowing you to log in">Change that</a></p>
<p>
You are not logged into Apple Music. We therefore can't show you your playlists.
<a href="" title="Refreshes the page, allowing you to log in">Change that</a>
</p>
<p>Use the button below to load songs from your local disk</p>
<input
id="pl-loader"

View File

@@ -1,7 +1,7 @@
// These functions handle connections to the backend with socket.io
import {
io, type Socket
type Socket, io
} from 'socket.io-client';
import type {
SSEMap

View File

@@ -1,7 +1,7 @@
// These functions handle connections to the backend with socket.io
import {
io, type Socket
type Socket, io
} from 'socket.io-client';
import type {
SSEMap
@@ -57,7 +57,8 @@ class NotificationHandler {
*/
connect ( roomName: string, useAntiTamper: boolean ): Promise<void> {
return new Promise( ( resolve, reject ) => {
fetch( localStorage.getItem( 'url' ) + '/createRoomToken?roomName=' + roomName + '&useAntiTamper=' + useAntiTamper, {
fetch( localStorage.getItem( 'url' ) + '/createRoomToken?roomName='
+ roomName + '&useAntiTamper=' + useAntiTamper, {
'credentials': 'include'
} ).then( res => {
if ( res.status === 200 ) {
@@ -110,7 +111,8 @@ class NotificationHandler {
'credentials': 'include'
} ).then( res => {
if ( res.status === 200 ) {
this.eventSource = new EventSource( localStorage.getItem( 'url' ) + '/socket/connection?room=' + this.roomName, {
this.eventSource = new EventSource( localStorage.getItem( 'url' )
+ '/socket/connection?room=' + this.roomName, {
'withCredentials': true
} );
@@ -118,7 +120,8 @@ class NotificationHandler {
this.isConnected = true;
this.connectionWasSuccessful = true;
this.reconnectRetryCount = 0;
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Connection successfully established!' );
console.log( '[ SSE Connection ] - '
+ new Date().toISOString() + ': Connection successfully established!' );
resolve();
};
@@ -135,8 +138,10 @@ class NotificationHandler {
this.isConnected = false;
this.eventSource?.close();
this.openConnectionsCount -= 1;
console.debug( e );
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Reconnecting due to connection error!' );
console.debug( '[ SSE Connection ] - Error encountered: ', e );
console.log( '[ SSE Connection ] - '
+ new Date().toISOString()
+ ': Reconnecting due to connection error!' );
this.eventSource = undefined;
@@ -146,7 +151,8 @@ class NotificationHandler {
}, 1000 * this.reconnectRetryCount );
}
};
} else if ( res.status === 403 || res.status === 401 || res.status === 404 || res.status === 402 ) {
} else if ( res.status === 403 || res.status === 401
|| res.status === 404 || res.status === 402 ) {
document.dispatchEvent( new Event( 'musicplayer:autherror' ) );
reject( 'ERR_UNAUTHORIZED' );
} else {
@@ -158,7 +164,8 @@ class NotificationHandler {
reject( 'ERR_ROOM_CONNECTING' );
} else {
this.openConnectionsCount -= 1;
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Reconnecting due to severe connection error!' );
console.log( '[ SSE Connection ] - ' + new Date().toISOString()
+ ': Reconnecting due to severe connection error!' );
this.eventSource = undefined;

View File

@@ -5,22 +5,32 @@
} from 'vue';
interface FullConfig {
'offering': BarConfig,
'ages': Ages
'offering': Bars;
'ages': Ages;
}
interface Bars {
[name: string]: {
'offering': BarConfig;
'name': string;
'id': string;
}
}
interface Ages {
'18+': string,
'16-18': string
'18+': string;
'16-18': string;
}
interface BarConfig {
[id: string]: Offer;
[id: string]: Offer
}
interface Offer {
'name': string;
'price': number; // In cents
'depot'?: number; // In cents
'showLine'?: boolean;
'id': string;
}
@@ -33,25 +43,38 @@
'16-18': '',
'below': ''
} );
const offering: Ref<BarConfig> = ref( {} );
const offering: Ref<Bars> = ref( {} );
const selection: Ref<Selection> = ref( {} );
const selectedBar: Ref<string> = ref( '' );
const enableDepotReminder = ref( true );
fetch( '/bar-config.json' ).then( res => {
let cashinInDepot = false;
fetch( '/bar-config.json', {
'cache': 'no-store'
} ).then( res => {
if ( res.status === 200 ) {
res.json().then( json => {
const data: FullConfig = json;
offering.value = data.offering;
ages.value = data.ages;
reset();
} );
} else {
alert( 'Failed to load' );
}
} );
const reset = () => {
const keys = Object.keys( offering.value );
const reset = ( skipCheck = true ) => {
if ( !skipCheck && !Object.keys( offering.value ).includes( selectedBar.value ) ) return;
if ( cashinInDepot && enableDepotReminder.value ) alert( 'Hand out chips for depot' );
cashinInDepot = false;
const keys = Object.keys( offering.value[ selectedBar.value ].offering );
selection.value = {};
keys.forEach( val => {
selection.value[ val ] = 0;
@@ -62,14 +85,22 @@
const keys = Object.keys( selection.value );
let totalPrice = 0;
let totalDepot = 0;
for ( let i = 0; i < keys.length; i++ ) {
const o = selection.value[ keys[ i ] ];
totalPrice += o * offering.value[ keys[ i ] ].price;
totalPrice += o * offering.value[ selectedBar.value ].offering[ keys[ i ] ].price;
totalDepot += o * ( offering.value[ selectedBar.value ].offering[ keys[ i ] ].depot ?? 0 );
}
return totalPrice / 100;
if ( totalDepot > 0 ) {
cashinInDepot = true;
}
totalPrice += totalDepot;
return ( totalPrice / 100 ) + ( totalDepot ? ` (Depot = ${ totalDepot })` : '' );
} );
const changeValue = ( id: string, amount: number ) => {
@@ -83,17 +114,40 @@
<template>
<div class="bar-utility">
<h1>Bar utility</h1>
<div style="margin: 0">
<label> Depot chips reminder</label>
<input v-model="enableDepotReminder" type="checkbox">
</div>
<h1 style="margin: 15px;">
Bar utility
</h1>
<div>
<label for="bar-select">Select bar </label>
<select id="bar-select" v-model="selectedBar" @change="reset()">
<option v-for="bar in Object.values( offering )" :key="bar.id" :value="bar.id">
{{ bar.name }}
</option>
</select>
<button @click="reset( false )">
Reset
</button>
</div>
<p>Check ages! (18+: {{ ages[ '18+' ] }}, 16-18: {{ ages[ '16-18' ] }})</p>
<button @click="reset()">
Reset
</button>
<p>Total: CHF {{ total }}</p>
<table class="offering-wrapper">
<p v-if="Object.keys( offering ).includes( selectedBar )">
Total: CHF {{ total }}
</p>
<table v-if="Object.keys( offering ).includes( selectedBar )" class="offering-wrapper">
<tbody>
<tr v-for="offer in offering" :key="offer.id" class="offering">
<tr
v-for="offer in offering[ selectedBar ].offering"
:key="offer.id"
:class="[ 'offering', offer.showLine ? 'show-line' : '' ]"
>
<td>
<p>{{ offer.name }} (CHF {{ offer.price / 100 }})</p>
<p>
{{ offer.name }} (CHF {{ offer.price / 100 }}{{
offer.depot ? ' + ' + ( offer.depot / 100 ) : '' }})
</p>
</td>
<td>
<div>
@@ -109,6 +163,9 @@
</tr>
</tbody>
</table>
<p v-if="Object.keys( offering ).includes( selectedBar )">
Total: CHF {{ total }}
</p>
</div>
</template>
@@ -120,7 +177,16 @@
flex-direction: column;
>.offering-wrapper {
border-collapse: collapse;
margin-bottom: 5vh;
.offering {
&.show-line {
>td {
border-bottom: solid 1px black;
}
}
>td {
padding: 5px;
p {

View File

@@ -7,7 +7,7 @@
<div v-if="hasLoaded && !showCouldNotFindRoom" style="width: 100%">
<div class="current-song-wrapper">
<img
v-if="playlist[ playingSong ]"
v-if="playlist[ playingSong ] && playlist[ playingSong ].cover"
id="current-image"
:src="playlist[ playingSong ].cover"
class="fancy-view-song-art"

View File

@@ -19,7 +19,7 @@
<div v-if="hasLoaded && !showCouldNotFindRoom" class="showcase-wrapper">
<div class="current-song-wrapper">
<img
v-if="playlist[ playingSong ]"
v-if="playlist[ playingSong ] && playlist[ playingSong ].cover"
id="current-image"
:src="playlist[ playingSong ].cover"
class="fancy-view-song-art"
@@ -52,7 +52,8 @@
</div>
<div class="song-list-wrapper">
<div v-for="song in songQueue" :key="song.id" class="song-list">
<img :src="song.cover" class="song-image">
<img v-if="song.cover" :src="song.cover" class="song-image">
<span v-else class="material-symbols-outlined song-cover">music_note</span>
<div
v-if="( playlist[ playingSong ] ? playlist[ playingSong ].id : '' ) === song.id && isPlaying"
class="playing-symbols"
@@ -97,7 +98,7 @@
Song
} from '@/scripts/song';
import {
computed, ref, type Ref
type Ref, computed, ref
} from 'vue';
import bizualizer from '@/scripts/bizualizer';

View File

@@ -21,6 +21,7 @@ import storeSDK from '@janishutz/store-sdk';
import sdk from '@janishutz/login-sdk-server';
import sse from './sse';
import socket from './socket';
import logger from './logger';
// const isFossVersion = true;
//
@@ -42,15 +43,13 @@ const run = () => {
const httpServer = createServer( app );
if ( !isFossVersion ) {
console.error( '[ APP ] Starting in non-FOSS version' );
logger.info( '[ APP ] Starting in non-FOSS version' );
const storeConfig = JSON.parse( fs.readFileSync( path.join(
__dirname,
'/config/store-sdk.config.secret.json'
) ).toString() );
console.error( storeConfig );
storeSDK.configure( storeConfig );
// ───────────────────────────────────────────────────────────────────
@@ -139,6 +138,7 @@ const run = () => {
'useAntiTamper': request.query.useAntiTamper === 'true'
? true : false,
};
logger.debug( `Created room "${ roomName }"` );
response.send( roomToken );
} else {
if (
@@ -211,7 +211,7 @@ const run = () => {
} else {
storeSDK.getSubscriptions( uid )
.then( stat => {
console.error( 'Subscription check was successful' );
logger.log( 'Subscription check was successful' );
const now = new Date().getTime();
for ( const sub in stat ) {
@@ -232,7 +232,7 @@ const run = () => {
resolve( false );
} )
.catch( e => {
console.error( 'Subscription check unsuccessful with error', e );
logger.error( 'Subscription check unsuccessful with error', e );
reject( 'ERR_NOT_OWNED' );
} );
}

104
backend/src/logger.ts Normal file
View File

@@ -0,0 +1,104 @@
import {
writeFile
} from 'node:fs';
const log = ( ...msg: string[] ) => {
output( 'log', log.caller.toString(), ...msg );
};
const info = ( ...msg: string[] ) => {
output( 'info', log.caller.toString(), ...msg );
};
const debug = ( ...msg: string[] ) => {
output( 'debug', log.caller.toString(), ...msg );
};
const warn = ( ...msg: string[] ) => {
output( 'warn', log.caller.toString(), ...msg );
};
const error = ( ...msg: string[] ) => {
output( 'error', log.caller.toString(), ...msg );
};
const fatal = ( ...msg: string[] ) => {
output( 'fatal', log.caller.toString(), ...msg );
};
let loc = 'stderr';
let lev = 0;
type LogLevel = 'debug' | 'info' | 'log' | 'warn' | 'error' | 'fatal';
const levels = [
'debug',
'info',
'log',
'warn',
'error',
'fatal'
];
const configure = ( location: 'stderr' | 'file', minLevel: LogLevel, file?: string ) => {
if ( location === 'file' && !file ) {
throw new Error( 'File parameter required when location is "file"' );
}
loc = location === 'stderr' ? 'stderr' : file;
lev = levels.indexOf( minLevel );
};
const logfile: string[] = [];
const output = ( level: LogLevel, caller: string, ...message: string[] ) => {
if ( levels.indexOf( level ) < lev ) {
return;
}
const msg = message.join( ' ' );
const out = `[${ level.toUpperCase() }] (${ new Date().toISOString() }) in ${ caller }: ${ msg }`;
if ( loc === 'stderr' ) {
console.error( out );
} else {
logfile.push( out );
save();
}
};
let isSaving = false;
let waitingOnSave = false;
const save = () => {
if ( isSaving ) {
waitingOnSave = true;
return;
}
isSaving = true;
writeFile( loc, JSON.stringify( logfile ), err => {
if ( err )
console.error( '[LOGGER] Failed to save with error ' + err );
if ( waitingOnSave ) {
waitingOnSave = false;
isSaving = false;
save();
}
isSaving = false;
} );
};
export default {
log,
info,
debug,
warn,
error,
fatal,
configure
};

View File

@@ -4,6 +4,7 @@ import bodyParser from 'body-parser';
import {
SocketData
} from './definitions';
import logger from './logger';
const useSSE = (
app: express.Application,
@@ -86,7 +87,11 @@ const useSSE = (
for ( const c in cl ) {
if ( cl[ c ] === sid ) {
cl.splice( parseInt( c ), 1 );
try {
cl.splice( parseInt( c ), 1 );
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch ( _ ) { /* empty */ }
break;
}
}
@@ -120,12 +125,13 @@ const useSSE = (
( request: express.Request, response: express.Response ) => {
if ( request.query.room ) {
if ( socketData[ String( request.query.room ) ] ) {
logger.debug( `Room "${ request.query.room }" was joined` );
response.send( 'ok' );
} else {
response.status( 404 ).send( 'ERR_ROOM_NOT_FOUND' );
}
} else {
response.status( 404 ).send( 'ERR_NO_ROOM_SPECIFIED' );
response.status( 400 ).send( 'ERR_NO_ROOM_SPECIFIED' );
}
}
);
@@ -138,8 +144,17 @@ const useSSE = (
( request: express.Request, response: express.Response ) => {
if ( socketData[ request.body.roomName ] ) {
if ( request.body.event === 'tampering' ) {
logger.debug( `Room "${
request.query.roomName }" has new event: Tampering` );
const clients = clientReference[ request.body.roomName ];
if ( !clients ) {
response.send( 'ERR_CANNOT_SEND' );
return;
}
for ( const client in clients ) {
if ( importantClients[ clients[ client ] ] ) {
importantClients[ clients[ client ] ]
@@ -181,9 +196,18 @@ const useSSE = (
.playlistIndex = request.body.data;
}
logger.debug( `Room "${
request.query.roomName }" has new event: ${ update }` );
if ( send ) {
const clients = clientReference[ request.body.roomName ];
if ( !clients ) {
response.send( 'ERR_CANNOT_SEND' );
return;
}
for ( const client in clients ) {
if ( connectedClients[ clients[ client ] ] ) {
connectedClients[ clients[ client ] ]
@@ -222,9 +246,17 @@ const useSSE = (
socketData[ request.body.roomName ].roomToken
=== request.body.roomToken
) {
logger.debug( `Room "${
request.query.roomName }" was deleted` );
socketData[ request.body.roomName ] = undefined;
const clients = clientReference[ request.body.roomName ];
if ( !clients ) {
response.send( 'ok' );
return;
}
for ( const client in clients ) {
if ( connectedClients[ clients[ client ] ] ) {
connectedClients[ clients[ client ] ]
@@ -234,6 +266,8 @@ const useSSE = (
} ) + '\n\n' );
}
}
response.send( 'ok' );
} else {
response.send( 403 ).send( 'ERR_UNAUTHORIZED' );
}

View File

@@ -10,6 +10,7 @@
import path from 'path';
import fs from 'fs';
import * as sqlDB from './mysqldb.js';
import logger from '../logger.js';
declare let __dirname: string | undefined;
@@ -33,9 +34,9 @@ dbh.connect();
*/
const initDB = (): undefined => {
( async () => {
console.log( '[ DB ] Setting up...' );
logger.info( '[ DB ] Setting up...' );
dbh.setupDB();
console.log( '[ DB ] Setting up complete!' );
logger.info( '[ DB ] Setting up complete!' );
} )();
};

View File

@@ -10,6 +10,7 @@
import mysql from 'mysql';
import fs from 'fs';
import path from 'path';
import logger from '../logger';
declare let __dirname: string | undefined;
@@ -63,20 +64,20 @@ class SQLDB {
const self = this;
if ( this.isRecovering ) {
console.log( '[ SQL ] Attempting to recover from critical error' );
logger.info( '[ SQL ] Attempting to recover from critical error' );
this.sqlConnection = mysql.createConnection( this.config );
this.isRecovering = false;
}
this.sqlConnection.connect( err => {
if ( err ) {
console.error( '[ SQL ]: An error ocurred whilst connecting: ' + err.stack );
logger.error( '[ SQL ]: An error ocurred whilst connecting: ' + err.stack );
reject( err );
return;
}
console.log( '[ SQL ] Connected to database successfully' );
logger.info( '[ SQL ] Connected to database successfully' );
self.sqlConnection.on( 'error', err => {
if ( err.code === 'ECONNRESET' ) {
self.isRecovering = true;
@@ -85,7 +86,7 @@ class SQLDB {
self.connect();
}, 1000 );
} else {
console.error( err );
logger.error( err );
}
} );
resolve( 'connection' );