various changes + improvements

This commit is contained in:
2023-11-01 16:57:16 +01:00
parent 07e5ebe1ad
commit 6ac807e2f6
7 changed files with 324 additions and 4 deletions

View File

@@ -12,6 +12,7 @@
"core-js": "^3.8.3", "core-js": "^3.8.3",
"cors": "^2.8.5", "cors": "^2.8.5",
"music-metadata": "^7.13.0", "music-metadata": "^7.13.0",
"realtime-bpm-analyzer": "^3.2.1",
"vue": "^3.2.13", "vue": "^3.2.13",
"vue-router": "^4.0.3" "vue-router": "^4.0.3"
}, },
@@ -11102,6 +11103,11 @@
"node": ">=8.10.0" "node": ">=8.10.0"
} }
}, },
"node_modules/realtime-bpm-analyzer": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/realtime-bpm-analyzer/-/realtime-bpm-analyzer-3.2.1.tgz",
"integrity": "sha512-q2jlMLwkPlZsCJk/e8gTM8qlqcOITnpUTPbWdwPBgvKGzixIMh7VmseRqxULKFdJWOrgM0GLTXzWys6JDmiQMw=="
},
"node_modules/regenerate": { "node_modules/regenerate": {
"version": "1.4.2", "version": "1.4.2",
"resolved": "https://registry.npmmirror.com/regenerate/-/regenerate-1.4.2.tgz", "resolved": "https://registry.npmmirror.com/regenerate/-/regenerate-1.4.2.tgz",
@@ -23133,6 +23139,11 @@
"picomatch": "^2.2.1" "picomatch": "^2.2.1"
} }
}, },
"realtime-bpm-analyzer": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/realtime-bpm-analyzer/-/realtime-bpm-analyzer-3.2.1.tgz",
"integrity": "sha512-q2jlMLwkPlZsCJk/e8gTM8qlqcOITnpUTPbWdwPBgvKGzixIMh7VmseRqxULKFdJWOrgM0GLTXzWys6JDmiQMw=="
},
"regenerate": { "regenerate": {
"version": "1.4.2", "version": "1.4.2",
"resolved": "https://registry.npmmirror.com/regenerate/-/regenerate-1.4.2.tgz", "resolved": "https://registry.npmmirror.com/regenerate/-/regenerate-1.4.2.tgz",

View File

@@ -15,6 +15,7 @@
"core-js": "^3.8.3", "core-js": "^3.8.3",
"cors": "^2.8.5", "cors": "^2.8.5",
"music-metadata": "^7.13.0", "music-metadata": "^7.13.0",
"realtime-bpm-analyzer": "^3.2.1",
"vue": "^3.2.13", "vue": "^3.2.13",
"vue-router": "^4.0.3" "vue-router": "^4.0.3"
}, },

View File

@@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> <link rel="icon" href="<%= BASE_URL %>favicon.ico">
<link rel="stylesheet" href="/icon-font.css" /> <link rel="stylesheet" href="/icon-font.css" />
<script defer src="/jquery.min.js"></script>
<title><%= htmlWebpackPlugin.options.title %></title> <title><%= htmlWebpackPlugin.options.title %></title>
</head> </head>
<body> <body>

2
frontend/public/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -120,6 +120,7 @@ app.get( '/indexDirs', ( req, res ) => {
( async() => { ( async() => {
// TODO: Check for songlist.csv or songlist.json file and use the data provided there for each song to override // TODO: Check for songlist.csv or songlist.json file and use the data provided there for each song to override
// what was found automatically. If no song title was found in songlist or metadata, use filename // what was found automatically. If no song title was found in songlist or metadata, use filename
// TODO: Also save found information to those files and don't rerun checks if data is present
let files = {}; let files = {};
for ( let file in dat ) { for ( let file in dat ) {
if ( allowedFileTypes.includes( dat[ file ].slice( dat[ file ].indexOf( '.' ), dat[ file ].length ) ) ) { if ( allowedFileTypes.includes( dat[ file ].slice( dat[ file ].indexOf( '.' ), dat[ file ].length ) ) ) {
@@ -148,6 +149,7 @@ app.get( '/indexDirs', ( req, res ) => {
files[ req.query.dir + '/' + dat[ file ] ][ 'hasCoverArt' ] = false; files[ req.query.dir + '/' + dat[ file ] ][ 'hasCoverArt' ] = false;
} }
} catch ( err ) { } catch ( err ) {
console.error( err );
files[ req.query.dir + '/' + dat[ file ] ] = 'ERROR'; files[ req.query.dir + '/' + dat[ file ] ] = 'ERROR';
} }
} else if ( dat[ file ].slice( dat[ file ].indexOf( '.' ), dat[ file ].length ) === '.csv' ) { } else if ( dat[ file ].slice( dat[ file ].indexOf( '.' ), dat[ file ].length ) === '.csv' ) {

View File

@@ -0,0 +1,281 @@
<!-- eslint-disable no-undef -->
<template>
<div id="notifications" @click="handleNotifications();">
<div class="message-box" :class="[ location, size ]">
<div class="message-container" :class="messageType">
<span class="material-symbols-outlined types hide" v-if="messageType == 'hide'">question_mark</span>
<span class="material-symbols-outlined types" v-else-if="messageType == 'ok'" style="background-color: green;">done</span>
<span class="material-symbols-outlined types" v-else-if="messageType == 'error'" style="background-color: red;">close</span>
<span class="material-symbols-outlined types progress-spinner" v-else-if="messageType == 'progress'" style="background-color: blue;">progress_activity</span>
<span class="material-symbols-outlined types" v-else-if="messageType == 'info'" style="background-color: lightblue;">info</span>
<span class="material-symbols-outlined types" v-else-if="messageType == 'warning'" style="background-color: orangered;">warning</span>
<p class="message">{{ message }}</p>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'notifications',
props: {
location: {
type: String,
'default': 'topleft',
},
size: {
type: String,
'default': 'default',
}
// Size options: small, default (default option), big, bigger, huge
},
data () {
return {
notifications: {},
queue: [],
message: '',
messageType: 'hide',
notificationDisplayTime: 0,
notificationPriority: 'normal',
currentlyDisplayedNotificationID: 0,
currentID: { 'critical': 0, 'medium': 1000, 'low': 100000 },
displayTimeCurrentNotification: 0,
notificationScheduler: null,
};
},
methods: {
createNotification( message, showDuration, messageType, priority ) {
/*
Takes a notification options array that contains: message, showDuration (in seconds), messageType (ok, error, progress, info) and priority (low, normal, critical).
Returns a notification ID which can be used to cancel the notification. The component will throttle notifications and display
one at a time and prioritize messages with higher priority. Use vue refs to access these methods.
*/
let id = 0;
if ( priority === 'critical' ) {
this.currentID[ 'critical' ] += 1;
id = this.currentID[ 'critical' ];
} else if ( priority === 'normal' ) {
this.currentID[ 'medium' ] += 1;
id = this.currentID[ 'medium' ];
} else if ( priority === 'low' ) {
this.currentID[ 'low' ] += 1;
id = this.currentID[ 'low' ];
}
this.notifications[ id ] = { 'message': message, 'showDuration': showDuration, 'messageType': messageType, 'priority': priority, 'id': id };
this.queue.push( id );
console.log( 'scheduled notification: ' + id + ' (' + message + ')' );
if ( this.displayTimeCurrentNotification >= this.notificationDisplayTime ) {
this.handleNotifications();
}
return id;
},
cancelNotification ( id ) {
/*
This method deletes a notification and, in case the notification is being displayed, hides it.
*/
try {
delete this.notifications[ id ];
} catch ( error ) {
console.log( 'notification to be deleted is nonexistent or currently being displayed' );
}
try {
this.queue.splice( this.queue.indexOf( id ), 1 );
} catch {
console.debug( 'queue empty' );
}
if ( this.currentlyDisplayedNotificationID == id ) {
this.handleNotifications();
}
},
handleNotifications () {
/*
This methods should NOT be called in any other component than this one!
*/
this.displayTimeCurrentNotification = 0;
this.notificationDisplayTime = 0;
this.message = '';
this.queue.sort();
if ( this.queue.length > 0 ) {
this.message = this.notifications[ this.queue[ 0 ] ][ 'message' ];
this.messageType = this.notifications[ this.queue[ 0 ] ][ 'messageType' ];
this.priority = this.notifications[ this.queue[ 0 ] ][ 'priority' ];
this.currentlyDisplayedNotificationID = this.notifications[ this.queue[ 0 ] ][ 'id' ];
this.notificationDisplayTime = this.notifications[ this.queue[ 0 ] ][ 'showDuration' ];
delete this.notifications[ this.queue[ 0 ] ];
this.queue.reverse();
this.queue.pop();
$( '.message-box' ).css( 'z-index', 20 );
} else {
this.messageType = 'hide';
$( '.message-box' ).css( 'z-index', -1 );
}
}
},
created () {
this.notificationScheduler = setInterval( () => {
if ( this.displayTimeCurrentNotification >= this.notificationDisplayTime ) {
this.handleNotifications();
} else {
this.displayTimeCurrentNotification += 0.5;
}
}, 500 );
},
unmounted ( ) {
clearInterval( this.notificationScheduler );
}
};
</script>
<style scoped>
.message-box {
position: fixed;
z-index: -1;
color: white;
transition: all 0.5s;
width: 95vw;
right: 2.5vw;
top: 1vh;
height: 10vh;
}
.message-container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
opacity: 1;
transition: all 0.5s;
cursor: default;
}
.types {
color: white;
border-radius: 100%;
margin-right: auto;
margin-left: 5%;
padding: 1.5%;
font-size: 200%;
}
.message {
margin-right: 5%;
text-align: end;
}
.ok {
background-color: rgb(1, 71, 1);
}
.error {
background-color: rgb(114, 1, 1);
}
.info {
background-color: rgb(44, 112, 151);
}
.warning {
background-color: orange;
}
.hide {
opacity: 0;
}
.progress {
z-index: 20;
background-color: rgb(0, 0, 99);
}
.progress-spinner {
animation: spin 2s infinite linear;
}
@keyframes spin {
from {
transform: rotate( 0deg );
}
to {
transform: rotate( 720deg );
}
}
@media only screen and (min-width: 750px) {
.default {
height: 10vh;
width: 32vw;
}
.small {
height: 7vh;
width: 27vw;
}
.big {
height: 12vh;
width: 38vw;
}
.bigger {
height: 15vh;
width: 43vw;
}
.huge {
height: 20vh;
width: 50vw;
}
.topleft {
top: 3vh;
left: 0.5vw;
}
.topright {
top: 3vh;
right: 0.5vw;
}
.bottomright {
bottom: 3vh;
right: 0.5vw;
}
.bottomleft {
top: 3vh;
right: 0.5vw;
}
}
@media only screen and (min-width: 1500px) {
.default {
height: 10vh;
width: 15vw;
}
.small {
height: 7vh;
width: 11vw;
}
.big {
height: 12vh;
width: 17vw;
}
.bigger {
height: 15vh;
width: 20vw;
}
.huge {
height: 20vh;
width: 25vw;
}
}
</style>

View File

@@ -39,6 +39,7 @@
:shuffle="isShuffleEnabled" :repeatMode="repeatMode" :durationBeautified="durationBeautified" :shuffle="isShuffleEnabled" :repeatMode="repeatMode" :durationBeautified="durationBeautified"
:playbackPos="playbackPos" :playbackPosBeautified="playbackPosBeautified" :playbackPos="playbackPos" :playbackPosBeautified="playbackPosBeautified"
@posUpdate="pos => { setPos( pos ) }"></FancyView> @posUpdate="pos => { setPos( pos ) }"></FancyView>
<Notifications ref="notifications" size="bigger" location="topright"></Notifications>
</div> </div>
</template> </template>
@@ -121,7 +122,9 @@
<script> <script>
import FancyView from './fancyView.vue'; import FancyView from './fancyView.vue';
import Notifications from './notifications.vue';
import SliderView from './sliderView.vue'; import SliderView from './sliderView.vue';
import * as realtimeBPM from 'realtime-bpm-analyzer';
export default { export default {
data() { data() {
@@ -142,14 +145,16 @@ export default {
components: { components: {
SliderView, SliderView,
FancyView, FancyView,
Notifications,
}, },
methods: { methods: {
play( song, autoplay, doCrossFade = false ) { play( song, autoplay, doCrossFade = false ) {
this.playingSong = song; this.playingSong = song;
this.audioLoaded = true; this.audioLoaded = true;
this.init( doCrossFade, autoplay ); this.init( doCrossFade, autoplay, song.filename );
}, },
init( doCrossFade, autoplay ) { // TODO: Make function that connects to status service and add various warnings.
init( doCrossFade, autoplay, filename ) {
this.control( 'reset' ); this.control( 'reset' );
// TODO: make it support cross-fade // TODO: make it support cross-fade
setTimeout( () => { setTimeout( () => {
@@ -182,6 +187,22 @@ export default {
} }
} }
} }
if ( !this.playingSong.bpm ) {
const audioContext = new AudioContext();
fetch( 'http://localhost:8081/getSongFile?filename=' + filename ).then( res => {
res.arrayBuffer().then( buf => {
// The file is uploaded, now we decode it
audioContext.decodeAudioData( buf, audioBuffer => {
// The result is passed to the analyzer
realtimeBPM.analyzeFullBuffer( audioBuffer ).then( topCandidates => {
// Do something with the BPM
this.playingSong.bpm = topCandidates[ 0 ].tempo;
this.sendUpdate( 'playingSong' );
} );
});
} );
} );
}
}, 300 ); }, 300 );
}, },
sendUpdate( update ) { sendUpdate( update ) {
@@ -201,8 +222,8 @@ export default {
'charset': 'utf-8' 'charset': 'utf-8'
}, },
}; };
fetch( 'http://localhost:8081/statusUpdate', fetchOptions ).then( res => { fetch( 'http://localhost:8081/statusUpdate', fetchOptions ).catch( err => {
console.log( res ); console.error( err );
} ); } );
}, },
control( action ) { control( action ) {
@@ -273,6 +294,7 @@ export default {
this.$emit( 'update', { 'type': 'fancyView', 'status': false } ); this.$emit( 'update', { 'type': 'fancyView', 'status': false } );
} }
} else if ( action === 'songsLoaded' ) { } else if ( action === 'songsLoaded' ) {
this.$refs.notifications.createNotification( 'Songs loaded successfully', 5, 'ok', 'default' );
this.hasLoadedSongs = true; this.hasLoadedSongs = true;
} else if ( action === 'shuffleOff' ) { } else if ( action === 'shuffleOff' ) {
this.$emit( 'update', { 'type': 'shuffleOff' } ); this.$emit( 'update', { 'type': 'shuffleOff' } );