mirror of
https://github.com/janishutz/MusicPlayerV2.git
synced 2025-11-25 04:54:23 +00:00
various changes + improvements
This commit is contained in:
11
frontend/package-lock.json
generated
11
frontend/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
2
frontend/public/jquery.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -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' ) {
|
||||||
|
|||||||
281
frontend/src/components/notifications.vue
Normal file
281
frontend/src/components/notifications.vue
Normal 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>
|
||||||
@@ -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' } );
|
||||||
|
|||||||
Reference in New Issue
Block a user