diff --git a/MusicPlayerV2-GUI/package-lock.json b/MusicPlayerV2-GUI/package-lock.json index 0d467ec..8ec7d42 100644 --- a/MusicPlayerV2-GUI/package-lock.json +++ b/MusicPlayerV2-GUI/package-lock.json @@ -11,6 +11,7 @@ "@esbuild-plugins/node-globals-polyfill": "^0.2.3", "@esbuild-plugins/node-modules-polyfill": "^0.2.2", "@jridgewell/sourcemap-codec": "^1.4.15", + "@melloware/coloris": "^0.24.0", "@rollup/plugin-inject": "^5.0.5", "buffer": "^6.0.3", "colorthief": "^2.2.0", @@ -588,6 +589,12 @@ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", "license": "MIT" }, + "node_modules/@melloware/coloris": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@melloware/coloris/-/coloris-0.24.0.tgz", + "integrity": "sha512-9RGKHqZJsUSsxb/0xaBCK5OKywobiK/xRtV8f4KQDmviqmVfkMLR3kK4DRuTTLSFdSOqkV0OQ/Niitu+rlXXYw==", + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/MusicPlayerV2-GUI/package.json b/MusicPlayerV2-GUI/package.json index cc75698..9a8fc7c 100644 --- a/MusicPlayerV2-GUI/package.json +++ b/MusicPlayerV2-GUI/package.json @@ -15,6 +15,7 @@ "@esbuild-plugins/node-globals-polyfill": "^0.2.3", "@esbuild-plugins/node-modules-polyfill": "^0.2.2", "@jridgewell/sourcemap-codec": "^1.4.15", + "@melloware/coloris": "^0.24.0", "@rollup/plugin-inject": "^5.0.5", "buffer": "^6.0.3", "colorthief": "^2.2.0", diff --git a/MusicPlayerV2-GUI/src/components/notificationsModule.vue b/MusicPlayerV2-GUI/src/components/notificationsModule.vue index fe336be..d1a4cfc 100644 --- a/MusicPlayerV2-GUI/src/components/notificationsModule.vue +++ b/MusicPlayerV2-GUI/src/components/notificationsModule.vue @@ -71,9 +71,9 @@ * @param {string} priority The priority of the message: 'low', 'normal', 'critical' * @returns {number} */ - const createNotification = ( message: string, showDuration: number, messageType: string, priority: string, redirect?: string ): number => { + const createNotification = ( message: string, showDuration: number, msgType: string, priority: string, redirect?: string ): number => { /* - Takes a notification options array that contains: message, showDuration (in seconds), messageType (ok, error, progress, info) and priority (low, normal, critical). + Takes a notification options array that contains: message, showDuration (in seconds), msgType (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. */ @@ -89,10 +89,10 @@ currentID.value[ 'low' ] += 1; id = currentID.value[ 'low' ]; } - notifications.value[ id ] = { 'message': message, 'showDuration': showDuration, 'messageType': messageType, 'priority': priority, 'id': id, redirect: redirect }; + notifications.value[ id ] = { 'message': message, 'showDuration': showDuration, 'messageType': msgType, 'priority': priority, 'id': id, redirect: redirect }; queue.value.push( id ); console.log( 'scheduled notification: ' + id + ' (' + message + ')' ); - if ( ( new Date().getTime() - notificationDisplayStartTime.value ) / 1000 >= ( notifications.value[ currentDID.value ] ? notifications.value[ currentDID.value ].showDuration : 0 ) ) { + if ( ( new Date().getTime() - notificationDisplayStartTime.value ) / 1000 >= ( notifications.value[ currentDID.value ] ? notifications.value[ currentDID.value ].showDuration : 0 ) || messageType.value === 'hide' ) { handleNotifications(); } return id; diff --git a/MusicPlayerV2-GUI/src/components/playerView.vue b/MusicPlayerV2-GUI/src/components/playerView.vue index 5ab96bc..7217701 100644 --- a/MusicPlayerV2-GUI/src/components/playerView.vue +++ b/MusicPlayerV2-GUI/src/components/playerView.vue @@ -31,13 +31,18 @@

{{ nicePlaybackPos }}

{{ niceDuration }}

- +
repeat{{ repeatMode }} - share - close +
+ share +
+ close + info +
+
shuffle{{ shuffleMode }}
@@ -52,6 +57,7 @@ @add-new-songs-apple-music="( song ) => addNewSongFromObject( song )"> + @@ -69,6 +75,7 @@ import notificationsModule from './notificationsModule.vue'; import { useUserStore } from '@/stores/userStore'; import NotificationHandler from '@/scripts/notificationHandler'; + import popupModule from './popupModule.vue'; const isPlaying = ref( false ); const repeatMode = ref( '' ); @@ -91,6 +98,9 @@ const notifications = ref( notificationsModule ); const notificationHandler = new NotificationHandler(); const isConnectedToNotifier = ref( false ); + const popup = ref( popupModule ); + const roomName = ref( '' ); + let currentlyOpenPopup = ''; const emits = defineEmits( [ 'playerStateChange' ] ); @@ -103,7 +113,12 @@ player.control( 'pause' ); stopProgressTracker(); } - notificationHandler.emit( 'playback', isPlaying.value ); + } + + const goToPos = ( position: number ) => { + player.goToPos( position ); + pos.value = position; + notificationHandler.emit( 'playback-start-update', new Date().getTime() - pos.value * 1000 ); } const toggleRemaining = () => { @@ -115,12 +130,12 @@ isPlaying.value = false; player.control( 'pause' ); stopProgressTracker(); - notificationHandler.emit( 'playback', isPlaying.value ); + notificationHandler.emit( 'playback-update', isPlaying.value ); } else if ( action === 'play' ) { isPlaying.value = true; player.control( 'play' ); startProgressTracker(); - notificationHandler.emit( 'playback', isPlaying.value ); + notificationHandler.emit( 'playback-update', isPlaying.value ); } else if ( action === 'repeat' ) { if ( repeatMode.value === '' ) { repeatMode.value = '_on'; @@ -137,29 +152,29 @@ shuffleMode.value = '_on'; player.setShuffle( true ); getDetails(); - notificationHandler.emit( 'playlist', playlist.value ); + notificationHandler.emit( 'playlist-update', playlist.value ); } else { shuffleMode.value = ''; player.setShuffle( false ); getDetails(); - notificationHandler.emit( 'playlist', playlist.value ); + notificationHandler.emit( 'playlist-update', playlist.value ); } getDetails(); } else if ( action === 'forward' ) { clickCountForward.value += 1; if( player.control( 'skip-10' ) ) { - setTimeout( () => { - startProgressTracker(); - getDetails(); - }, 2000 ); + startProgressTracker(); + } else { + pos.value = player.getPlaybackPos(); + notificationHandler.emit( 'playback-start-update', new Date().getTime() - pos.value * 1000 ); } } else if ( action === 'back' ) { clickCountBack.value += 1; if( player.control( 'back-10' ) ) { - setTimeout( () => { - startProgressTracker(); - getDetails(); - }, 2000 ); + startProgressTracker(); + } else { + pos.value = player.getPlaybackPos(); + notificationHandler.emit( 'playback-start-update', new Date().getTime() - pos.value * 1000 ); } } else if ( action === 'next' ) { stopProgressTracker(); @@ -167,27 +182,39 @@ coverArt.value = ''; currentlyPlayingSongArtist.value = ''; currentlyPlayingSongName.value = 'Loading...'; - setTimeout( () => { - getDetails(); - startProgressTracker(); - }, 2000 ); + startProgressTracker(); } else if ( action === 'previous' ) { stopProgressTracker(); player.control( 'previous' ); coverArt.value = ''; currentlyPlayingSongArtist.value = ''; currentlyPlayingSongName.value = 'Loading...'; - setTimeout( () => { - getDetails(); - startProgressTracker(); - }, 2000 ); + startProgressTracker(); } else if ( action === 'start-share' ) { - // TODO: Open popup, then send data with popup returns - notificationHandler.connect( 'test' ); + popup.value.openPopup( { + title: 'Define a share name', + popupType: 'input', + subtitle: 'A share allows others to join your playlist and see the current song, the playback position and the upcoming songs. You can get the link to the page, once the share is set up. Please choose a name, which will then be part of the URL with which others can join the share', + data: [ + { + name: 'Share Name', + dataType: 'text', + id: 'roomName' + } + ] + } ); + currentlyOpenPopup = 'create-share'; } else if ( action === 'stop-share' ) { if ( confirm( 'Do you really want to stop sharing?' ) ) { notificationHandler.disconnect(); + isConnectedToNotifier.value = false; + notifications.value.createNotification( 'Disconnected successfully!', 5, 'ok', 'normal' ); } + } else if ( action === 'show-share' ) { + alert( 'You are currently connected to share "' + roomName.value + + '". \nYou can connect to it via https://music.janishutz.com/share/' + roomName.value + + '. \n\nYou can connect to the fancy showcase screen using this link: https://music.janishutz.com/fancy/' + roomName.value + + '. Be aware that this one will use significantly more system AND network resources, so only use that for a screen that is front and center, not for a QR code to have all people connect to.' ); } } @@ -230,11 +257,10 @@ currentlyPlayingSongName.value = 'Loading...'; player.setPlaylistByID( id ).then( () => { isPlaying.value = true; + startProgressTracker(); setTimeout( () => { - startProgressTracker(); getDetails(); - notificationHandler.emit( 'playlist', playlist.value ); - // TODO: Add playback-start emit as well. For every time elapsed before starting to play current section, move start time back + notificationHandler.emit( 'playlist-update', playlist.value ); }, 2000 ); } ); } @@ -255,9 +281,10 @@ player.setPlaylist( playlist.value ); player.prepare( 0 ); isPlaying.value = true; + startProgressTracker(); setTimeout( () => { - startProgressTracker(); getDetails(); + notificationHandler.emit( 'playlist-update', playlist.value ); }, 2000 ); notifications.value.cancelNotification( n ); notifications.value.createNotification( 'Playlist loaded', 10, 'ok', 'normal' ); @@ -316,10 +343,7 @@ for ( const s in p ) { if ( p[ s ].id === id ) { player.prepare( parseInt( s ) ); - setTimeout( () => { - getDetails(); - startProgressTracker(); - }, 2000 ); + startProgressTracker(); break; } } @@ -332,8 +356,9 @@ const startProgressTracker = () => { hasReachedEnd = false; isPlaying.value = true; - const playingSong = player.getPlayingSong(); - prepNiceDurationTime( playingSong ); + let playingSong = player.getPlayingSong(); + hasStarted = false; + pos.value = 0; progressTracker = setInterval( () => { pos.value = player.getPlaybackPos(); if ( pos.value > playingSong.duration - 1 && !hasReachedEnd ) { @@ -350,9 +375,13 @@ } if ( pos.value > 0 && !hasStarted ) { - notificationHandler.emit( 'playlist-index', currentlyPlayingSongIndex.value ); - notificationHandler.emit( 'playback', isPlaying.value ); - notificationHandler.emit( 'playback-start', new Date().getTime() - pos.value * 1000 ); + getDetails(); + playingSong = player.getPlayingSong(); + console.log( pos.value ); + 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 ); hasStarted = true; } @@ -404,11 +433,13 @@ clearInterval( progressTracker ); } catch ( _ ) { /* empty */ } isPlaying.value = false; + notificationHandler.emit( 'playback-update', isPlaying.value ); } const moveSong = ( move: SongMove ) => { player.moveSong( move ); getDetails(); + notificationHandler.emit( 'playlist-update', playlist.value ); } const addNewSongs = async ( songs: ReadFile[] ) => { @@ -426,13 +457,11 @@ if ( !isPlaying.value ) { player.prepare( 0 ); isPlaying.value = true; - setTimeout( () => { - startProgressTracker(); - getDetails(); - }, 2000 ); + startProgressTracker(); } notifications.value.cancelNotification( n ); notifications.value.createNotification( 'New songs added', 10, 'ok', 'normal' ); + notificationHandler.emit( 'playlist-update', playlist.value ); } const addNewSongFromObject = ( song: Song ) => { @@ -442,10 +471,8 @@ if ( !isPlaying.value ) { player.prepare( 0 ); isPlaying.value = true; - setTimeout( () => { - startProgressTracker(); - getDetails(); - }, 2000 ); + startProgressTracker(); + notificationHandler.emit( 'playlist-update', playlist.value ); } } @@ -468,6 +495,28 @@ } } ); + const popupReturnHandler = ( data: any ) => { + if ( currentlyOpenPopup === 'create-share' ) { + notificationHandler.connect( data.roomName ).then( () => { + roomName.value = notificationHandler.getRoomName(); + isConnectedToNotifier.value = true; + 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( 'playlist-update', playlist.value ); + notifications.value.createNotification( 'Joined share "' + data.roomName + '"!', 5, 'ok', 'normal' ); + } ).catch( e => { + if ( e === 'ERR_CONFLICT' ) { + notifications.value.createNotification( 'A share with this name exists already!', 5, 'error', 'normal' ); + control( 'start-share' ); + } else { + console.error( e ); + notifications.value.createNotification( 'Could not create share!', 5, 'error', 'normal' ); + } + } ); + } + } + defineExpose( { logIntoAppleMusic, getPlaylists, diff --git a/MusicPlayerV2-GUI/src/components/playlistView.vue b/MusicPlayerV2-GUI/src/components/playlistView.vue index 834f76f..fbf2fb8 100644 --- a/MusicPlayerV2-GUI/src/components/playlistView.vue +++ b/MusicPlayerV2-GUI/src/components/playlistView.vue @@ -188,7 +188,7 @@ .playlist-box { height: calc( 100% - 100px ); width: 100%; - overflow: scroll; + overflow-y: scroll; display: flex; align-items: center; flex-direction: column; diff --git a/MusicPlayerV2-GUI/src/components/popupModule.vue b/MusicPlayerV2-GUI/src/components/popupModule.vue index 63868e1..5457f2b 100644 --- a/MusicPlayerV2-GUI/src/components/popupModule.vue +++ b/MusicPlayerV2-GUI/src/components/popupModule.vue @@ -174,6 +174,7 @@ overflow: hidden; transition: all 0.5s; transform: scale(1); + z-index: 99; } .incomplete-message { @@ -195,7 +196,7 @@ padding: 2.5%; border-radius: 20px; position: relative; - overflow: scroll; + overflow-y: scroll; display: block; } @@ -211,7 +212,7 @@ .popup-content { position: unset; height: 60%; - overflow: scroll; + overflow-y: scroll; } .textarea { diff --git a/MusicPlayerV2-GUI/src/router/index.ts b/MusicPlayerV2-GUI/src/router/index.ts index 109dfe7..f7bd531 100644 --- a/MusicPlayerV2-GUI/src/router/index.ts +++ b/MusicPlayerV2-GUI/src/router/index.ts @@ -23,6 +23,24 @@ const router = createRouter( { 'title': 'App' } }, + { + path: '/share/:name', + name: 'share', + component: () => import( '../views/RemoteView.vue' ), + meta: { + 'authRequired': false, + 'title': 'Share' + } + }, + { + path: '/fancy/:name', + name: 'fancy', + component: () => import( '../views/ShowcaseView.vue' ), + meta: { + 'authRequired': false, + 'title': 'Fancy View' + } + }, { path: '/:pathMatch(.*)*', name: 'NotFound', diff --git a/MusicPlayerV2-GUI/src/scripts/bizualizer.ts b/MusicPlayerV2-GUI/src/scripts/bizualizer.ts index e69de29..90ae6f3 100644 --- a/MusicPlayerV2-GUI/src/scripts/bizualizer.ts +++ b/MusicPlayerV2-GUI/src/scripts/bizualizer.ts @@ -0,0 +1,141 @@ +import ColorThief from 'colorthief'; +const colorThief = new ColorThief(); + +const getImageData = (): Promise => { + return new Promise( ( resolve ) => { + const img = ( document.getElementById( 'current-image' ) as HTMLImageElement ); + console.log( img ); + if ( img.complete ) { + resolve( colorThief.getPalette( img ) ); + } else { + img.addEventListener( 'load', () => { + resolve( colorThief.getPalette( img ) ); + } ); + } + } ); +} + +const createBackground = () => { + return new Promise( ( resolve ) => { + getImageData().then( palette => { + console.log( palette ); + const colourDetails: number[][] = []; + const colours: string[] = []; + let differentEnough = true; + if ( palette[ 0 ] ) { + for ( const i in palette ) { + for ( const colour in colourDetails ) { + const colourDiff = ( Math.abs( colourDetails[ colour ][ 0 ] - palette[ i ][ 0 ] ) / 255 + + Math.abs( colourDetails[ colour ][ 1 ] - palette[ i ][ 1 ] ) / 255 + + Math.abs( colourDetails[ colour ][ 2 ] - palette[ i ][ 2 ] ) / 255 ) / 3 * 100; + if ( colourDiff > 15 ) { + differentEnough = true; + } + } + if ( differentEnough ) { + colourDetails.push( palette[ i ] ); + colours.push( 'rgb(' + palette[ i ][ 0 ] + ',' + palette[ i ][ 1 ] + ',' + palette[ i ][ 2 ] + ')' ); + } + differentEnough = false; + } + } + let outColours = 'conic-gradient('; + if ( colours.length < 3 ) { + for ( let i = 0; i < 3; i++ ) { + if ( colours[ i ] ) { + outColours += colours[ i ] + ','; + } else { + if ( i === 0 ) { + outColours += 'blue,'; + } else if ( i === 1 ) { + outColours += 'green,'; + } else if ( i === 2 ) { + outColours += 'red,'; + } + } + } + } else if ( colours.length < 11 ) { + for ( const i in colours ) { + outColours += colours[ i ] + ','; + } + } else { + for ( let i = 0; i < 10; i++ ) { + outColours += colours[ i ] + ','; + } + } + outColours += colours[ 0 ] ?? 'blue' + ')'; + resolve( outColours ); + } ); + } ); +} + +let callbackFun = () => {} +const subscribeToBeatUpdate = ( cb: () => void ) => { + callbackFun = cb; + micAudioHandler(); +} + +const unsubscribeFromBeatUpdate = () => { + callbackFun = () => {} + try { + clearInterval( micAnalyzer ); + } catch ( e ) { /* empty */ } +} + +const coolDown = () => { + beatDetected = false; +} + +let micAnalyzer = 0; +let beatDetected = false; +const micAudioHandler = () => { + const audioContext = new ( window.AudioContext || window.webkitAudioContext )(); + const analyser = audioContext.createAnalyser(); + analyser.fftSize = 256; + const bufferLength = analyser.frequencyBinCount; + const dataArray = new Uint8Array( bufferLength ); + beatDetected = false; + + navigator.mediaDevices.getUserMedia( { audio: true } ).then( ( stream ) => { + const mic = audioContext.createMediaStreamSource( stream ); + mic.connect( analyser ); + analyser.getByteFrequencyData( dataArray ); + let prevSpectrum: number[] = []; + const threshold = 10; // Adjust as needed + micAnalyzer = setInterval( () => { + analyser.getByteFrequencyData( dataArray ); + // Convert the frequency data to a numeric array + const currentSpectrum = Array.from( dataArray ); + + if ( prevSpectrum ) { + // Calculate the spectral flux + const flux = calculateSpectralFlux( prevSpectrum, currentSpectrum ); + + if ( flux > threshold && !beatDetected ) { + // Beat detected + beatDetected = true; + callbackFun(); + } + } + prevSpectrum = currentSpectrum; + }, 20 ); + } ); +} + +const calculateSpectralFlux = ( prevSpectrum: number[], currentSpectrum: number[] ) => { + let flux = 0; + + for ( let i = 0; i < prevSpectrum.length; i++ ) { + const diff = currentSpectrum[ i ] - prevSpectrum[ i ]; + flux += Math.max( 0, diff ); + } + + return flux; +} + +export default { + createBackground, + subscribeToBeatUpdate, + unsubscribeFromBeatUpdate, + coolDown, +} \ No newline at end of file diff --git a/MusicPlayerV2-GUI/src/scripts/connection.ts b/MusicPlayerV2-GUI/src/scripts/connection.ts new file mode 100644 index 0000000..5edacc4 --- /dev/null +++ b/MusicPlayerV2-GUI/src/scripts/connection.ts @@ -0,0 +1,81 @@ +/* +* MusicPlayerV2 - notificationHandler.ts +* +* Created by Janis Hutz 06/26/2024, Licensed under the GPL V3 License +* https://janishutz.com, development@janishutz.com +* +* +*/ + +// These functions handle connections to the backend with socket.io + +import { io, type Socket } from "socket.io-client" + +class SocketConnection { + socket: Socket; + roomName: string; + isConnected: boolean; + + constructor () { + this.socket = io( localStorage.getItem( 'url' ) ?? '', { + autoConnect: false, + } ); + this.roomName = location.pathname.split( '/' )[ 2 ]; + this.isConnected = false; + } + + /** + * Create a room token and connect to + * @returns {Promise} + */ + connect (): Promise { + return new Promise( ( resolve, reject ) => { + this.socket.connect(); + this.socket.emit( 'join-room', this.roomName, ( res: { status: boolean, msg: string, data: any } ) => { + if ( res.status === true ) { + this.isConnected = true; + resolve( res.data ); + } else { + console.debug( res.msg ); + reject( 'ERR_ROOM_CONNECTING' ); + } + } ); + } ); + } + + /** + * Emit an event + * @param {string} event The event to emit + * @param {any} data + * @returns {void} + */ + emit ( event: string, data: any ): void { + if ( this.isConnected ) { + this.socket.emit( event, { 'roomName': this.roomName, 'data': data } ); + } + } + + /** + * Register a listener function for an event + * @param {string} event The event to listen for + * @param {( data: any ) => void} cb The callback function / listener function + * @returns {void} + */ + registerListener ( event: string, cb: ( data: any ) => void ): void { + if ( this.isConnected ) { + this.socket.on( event, cb ); + } + } + + /** + * Disconnect from the server + * @returns {any} + */ + disconnect (): void { + if ( this.isConnected ) { + this.socket.disconnect(); + } + } +} + +export default SocketConnection; \ No newline at end of file diff --git a/MusicPlayerV2-GUI/src/scripts/notificationHandler.ts b/MusicPlayerV2-GUI/src/scripts/notificationHandler.ts index 5da9ec1..192ea36 100644 --- a/MusicPlayerV2-GUI/src/scripts/notificationHandler.ts +++ b/MusicPlayerV2-GUI/src/scripts/notificationHandler.ts @@ -15,6 +15,7 @@ class NotificationHandler { socket: Socket; roomName: string; roomToken: string; + isConnected: boolean; constructor () { this.socket = io( localStorage.getItem( 'url' ) ?? '', { @@ -22,6 +23,7 @@ class NotificationHandler { } ); this.roomName = ''; this.roomToken = ''; + this.isConnected = false; } /** @@ -41,7 +43,8 @@ class NotificationHandler { name: this.roomName, token: this.roomToken }, ( res: { status: boolean, msg: string } ) => { - if ( res.status === true) { + if ( res.status === true ) { + this.isConnected = true; resolve(); } else { reject( 'ERR_ROOM_CONNECTING' ); @@ -64,7 +67,9 @@ class NotificationHandler { * @returns {void} */ emit ( event: string, data: any ): void { - this.socket.emit( event, data ); + if ( this.isConnected ) { + this.socket.emit( event, { 'roomToken': this.roomToken, 'roomName': this.roomName, 'data': data } ); + } } /** @@ -74,7 +79,9 @@ class NotificationHandler { * @returns {void} */ registerListener ( event: string, cb: ( data: any ) => void ): void { - this.socket.on( event, cb ); + if ( this.isConnected ) { + this.socket.on( event, cb ); + } } /** @@ -82,15 +89,21 @@ class NotificationHandler { * @returns {any} */ disconnect (): void { - this.socket.disconnect(); - this.socket.emit( 'delete-room', { - name: this.roomName, - token: this.roomToken - }, ( res: { status: boolean, msg: string } ) => { - if ( !res.status ) { - alert( 'Unable to delete the room you were just in. The name will be blocked until the next server restart!' ); - } - } ); + if ( this.isConnected ) { + this.socket.emit( 'delete-room', { + name: this.roomName, + token: this.roomToken + }, ( res: { status: boolean, msg: string } ) => { + this.socket.disconnect(); + if ( !res.status ) { + alert( 'Unable to delete the room you were just in. The name will be blocked until the next server restart!' ); + } + } ); + } + } + + getRoomName (): string { + return this.roomName; } } diff --git a/MusicPlayerV2-GUI/src/scripts/player.ts b/MusicPlayerV2-GUI/src/scripts/player.ts deleted file mode 100644 index 69e2012..0000000 --- a/MusicPlayerV2-GUI/src/scripts/player.ts +++ /dev/null @@ -1,357 +0,0 @@ -// IMPORTANT: Old, unfinished version that doesn't ship! See ./music-player.ts for the actual code! - - -type Origin = 'apple-music' | 'disk'; - -interface Song { - /** - * The ID. Either the apple music ID, or if from local disk, an ID starting in local_ - */ - id: string; - - /** - * Origin of the song - */ - origin: Origin; - - /** - * The cover image as a URL - */ - cover: string; - - /** - * The artist of the song - */ - artist: string; - - /** - * The name of the song - */ - title: string; - - /** - * Duration of the song in milliseconds - */ - duration: number; - - /** - * (OPTIONAL) The genres this song belongs to. Can be displayed on the showcase screen, but requires settings there - */ - genres?: string[]; - - /** - * (OPTIONAL) This will be displayed in brackets on the showcase screens - */ - additionalInfo?: string; -} - -interface Config { - devToken: string; - userToken: string; -} - - - -class MusicKitJSWrapper { - playingSongID: number; - playlist: Song[]; - queue: number[]; - config: Config; - musicKit: any; - isLoggedIn: boolean; - isPreparedToPlay: boolean; - repeatMode: string; - isShuffleEnabled: boolean; - - constructor () { - this.playingSongID = 0; - this.playlist = []; - this.queue = []; - this.config = { - devToken: '', - userToken: '', - }; - this.isShuffleEnabled = false; - this.repeatMode = ''; - this.isPreparedToPlay = false; - this.isLoggedIn = false; - - const self = this; - - if ( !window.MusicKit ) { - document.addEventListener( 'musickitloaded', () => { - self.init(); - } ); - } else { - this.init(); - } - } - - logIn () { - if ( !this.musicKit.isAuthorized ) { - this.musicKit.authorize().then( () => { - this.isLoggedIn = true; - this.init(); - } ); - } else { - this.musicKit.authorize().then( () => { - this.isLoggedIn = true; - this.init(); - } ); - } - } - - init () { - fetch( localStorage.getItem( 'url' ) + '/getAppleMusicDevToken', { credentials: 'include' } ).then( res => { - if ( res.status === 200 ) { - res.text().then( token => { - // MusicKit global is now defined - MusicKit.configure( { - developerToken: token, - app: { - name: 'MusicPlayer', - build: '2' - }, - storefrontId: 'CH', - } ).then( () => { - this.config.devToken = token; - this.musicKit = MusicKit.getInstance(); - if ( this.musicKit.isAuthorized ) { - this.isLoggedIn = true; - this.config.userToken = this.musicKit.musicUserToken; - } - this.musicKit.shuffleMode = MusicKit.PlayerShuffleMode.off; - this.apiGetRequest( 'https://api.music.apple.com/v1/me/library/playlists', this.handleAPIReturns ); - } ); - } ); - } - } ); - } - - handleAPIReturns ( data: object ) { - console.log( data ); - } - - getUserPlaylists () { - - } - - apiGetRequest ( url: string, callback: ( data: object ) => void ) { - if ( this.config.devToken != '' && this.config.userToken != '' ) { - fetch( url, { - method: 'GET', - headers: { - 'Authorization': `Bearer ${ this.config.devToken }`, - 'Music-User-Token': this.config.userToken - } - } ).then( res => { - if ( res.status === 200 ) { - res.json().then( json => { - try { - callback( { 'status': 'ok', 'data': json } ); - } catch( err ) { /* empty */} - } ); - } else { - try { - callback( { 'status': 'error', 'error': res.status } ); - } catch( err ) { /* empty */} - } - } ); - } else return false; - } - - /** - * Start playing the song at the current songID. - * @returns {void} - */ - play (): void { - this.musicKit.play(); - } - - /** - * Start playing the current song - * @returns {void} - */ - pause (): void { - this.musicKit.pause() - } - - /** - * Skip to the next song - * @returns {void} - */ - skip (): void { - if ( this.playingSongID < this.queue.length - 1 ) { - this.playingSongID += 1; - } else { - this.playingSongID = 0; - this.pause(); - } - } - - - /** - * Return to start of song, or if within four seconds of start of the song, go to previous song. - * @returns {void} - */ - previous (): void { - if ( this.playingSongID > 0 ) { - this.playingSongID -= 1; - } else { - this.playingSongID = this.queue.length - 1; - } - } - - /** - * Go to a specific position in the song. If position > song duration, go to next song - * @param {number} pos The position in milliseconds since start of the song - * @returns {void} - */ - goToPos ( pos: number ): void { - // TODO: Implement for non-apple-music too - if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) { - this.musicKit.seekToTime( pos ); - } - } - - /** - * Set, if the queue should be shuffled - * @param {boolean} enable True to enable shuffle, false to disable - * @returns {void} - */ - shuffle ( enable: boolean ): void { - this.isShuffleEnabled = enable; - this.preparePlaying( false ); - } - - /** - * Set the repeat mode - * @param {string} repeatType The repeat type. Can be '', '_on' or '_one_on' - * @returns {void} - */ - repeat ( repeatType: string ): void { - this.repeatMode = repeatType; - } - - /** - * Set the playlist to play. - * @param {Song[]} pl Playlist to play. An array of songs - * @returns {void} - */ - setPlaylist ( pl: Song[] ): void { - this.playlist = pl; - this.pause(); - this.playingSongID = 0; - this.queue = []; - } - - /** - * Prepare to play songs. Should be called whenever the playlist is changed or at beginning - * @param {boolean?} reset (OPTIONAL) Reset the players or keep playing, but shuffle playlist? - * @returns {void} - */ - preparePlaying ( reset?: boolean ): void { - this.queue = []; - this.isPreparedToPlay = true; - // TODO: finish - } - - /** - * Set which song (by Song-ID) to play. - * @param {string} id The song ID (apple music ID or internal ID, if from local drive) - * @returns {void} - */ - setCurrentlyPlayingSongID ( id: string ): void { - // TODO: Implement playlist etc handling - this.setPlayingSong( id, 'apple-music' ); - } - - /** - * Insert a song into the currently playing playlist - * @param {Song} song A song using the Song object - * @param {number} pos Position in the queue to insert it into - * @returns {void} - */ - insertSong ( song: Song, pos: number ): void { - - } - - /** - * Remove a song from the queue - * @param {string} id Song ID to remove. - * @returns {void} - */ - removeSong ( id: string ): void { - // TODO: Remove from queue too - } - - /** - * Get the playlist, as it will play - * @returns {Song[]} - */ - getOrderedPlaylist (): Song[] { - return this.playlist; - } - - /** - * Get the playlist, ignoring order specified by the queue. - * @returns {Song[]} - */ - getPlaylist (): Song[] { - return this.playlist; - } - - /** - * Get the position of the playback head. Returns time in ms - * @returns {number} - */ - getPlaybackPos (): number { - return 0; - } - - /** - * Returns the currently playing song object - * @returns {Song} - */ - getPlayingSong (): Song { - return this.playlist[ this.playingSongID ]; - } - - /** - * Returns the ID of the currently playing song - * @returns {string} - */ - getPlayingSongID (): string { - return this.playlist[ this.playingSongID ].id; - } - - /** - * Returns the index in the playlist of the currently playing song - * @returns {number} - */ - getPlayingIndex (): number { - return this.playingSongID; - } - - /** - * Set the currently playing song by Apple Music ID or disk path - * @param {string} id The ID of the song or disk path - * @param {Origin} origin The origin of the song. - * @returns {void} - */ - setPlayingSong ( id: string, origin: Origin ): void { - if ( origin === 'apple-music' ) { - this.musicKit.setQueue( { 'song': id } ).then( () => { - setTimeout( () => { - this.play(); - }, 500 ); - } ).catch( ( err ) => { - console.log( err ); - } ); - } else { - // TODO: Implement - } - } -} - -export default MusicKitJSWrapper; \ No newline at end of file diff --git a/MusicPlayerV2-GUI/src/stores/userStore.ts b/MusicPlayerV2-GUI/src/stores/userStore.ts index 7733039..4986d39 100644 --- a/MusicPlayerV2-GUI/src/stores/userStore.ts +++ b/MusicPlayerV2-GUI/src/stores/userStore.ts @@ -10,17 +10,17 @@ import { defineStore } from 'pinia'; export const useUserStore = defineStore( 'user', { - state: () => ( { 'isUserAuth': false, 'isAdminAuth': false, 'isUsingKeyboard': false, 'username': '' } ), + state: () => ( { 'isUserAuth': false, 'hasSubscribed': false, 'isUsingKeyboard': false, 'username': '' } ), getters: { getUserAuthenticated: ( state ) => state.isUserAuth, - getAdminAuthenticated: ( state ) => state.isAdminAuth, + getSubscriptionStatus: ( state ) => state.hasSubscribed, }, actions: { setUserAuth ( auth: boolean ) { this.isUserAuth = auth; }, - setAdminAuth ( auth: boolean ) { - this.isAdminAuth = auth; + setSubscriptionStatus ( status: boolean ) { + this.hasSubscribed = status; }, setUsername ( username: string ) { this.username = username; diff --git a/MusicPlayerV2-GUI/src/views/AppView.vue b/MusicPlayerV2-GUI/src/views/AppView.vue index 1f3d64d..fb5b9ee 100644 --- a/MusicPlayerV2-GUI/src/views/AppView.vue +++ b/MusicPlayerV2-GUI/src/views/AppView.vue @@ -1,6 +1,9 @@ @@ -25,6 +29,7 @@ const isShowingFullScreenPlayer = ref( false ); const player = ref( playerView ); const playlists = ref( [] ); + const hasFinishedLoading = ref( true ); const handlePlayerStateChange = ( newState: string ) => { if ( newState === 'hide' ) { @@ -66,6 +71,15 @@ const selectCustomPlaylist = ( playlist: ReadFile[] ) => { player.value.selectCustomPlaylist( playlist ); } + + // fetch( localStorage.getItem( 'url' ) + '/checkUserStatus', { credentials: 'include' } ).then( res => { + // if ( res.status === 200 ) { + // res.json().then( json => { + + // } ); + // } + // } ); + \ No newline at end of file diff --git a/MusicPlayerV2-GUI/src/views/ShowcaseView.vue b/MusicPlayerV2-GUI/src/views/ShowcaseView.vue index e949e1b..fc4b2dc 100644 --- a/MusicPlayerV2-GUI/src/views/ShowcaseView.vue +++ b/MusicPlayerV2-GUI/src/views/ShowcaseView.vue @@ -1,416 +1,479 @@ - \ No newline at end of file + const setBackground = () => { + bizualizer.createBackground().then( bg => { + $( '#background' ).css( 'background', bg ); + } ); + } + + const notifier = () => { + Notification.requestPermission(); + + console.warn( '[ notifier ]: Status is now enabled \n\n-> Any leaving or tampering with the website will send a notification to the host' ); + // Detect if window is currently in focus + window.onblur = () => { + sendNotification(); + } + + // Detect if browser window becomes hidden (also with blur event) + document.onvisibilitychange = () => { + if ( document.visibilityState === 'hidden' ) { + sendNotification(); + } + }; + } + + const sendNotification = () => { + new Notification( 'YOU ARE UNDER SURVEILLANCE', { + body: 'Please return to the original webpage immediately!', + requireInteraction: true, + } ); + } + + + + + \ No newline at end of file diff --git a/MusicPlayerV2-GUI/src/views/showcase-backup.txt b/MusicPlayerV2-GUI/src/views/showcase-backup.txt new file mode 100644 index 0000000..e949e1b --- /dev/null +++ b/MusicPlayerV2-GUI/src/views/showcase-backup.txt @@ -0,0 +1,416 @@ + + + + \ No newline at end of file diff --git a/backend/src/app.ts b/backend/src/app.ts index dc6709b..6201c85 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -119,16 +119,18 @@ const run = () => { } } ) - socket.on( 'playlist', ( data: { roomName: string, roomToken: string, data: Song[] } ) => { + socket.on( 'playlist-update', ( data: { roomName: string, roomToken: string, data: Song[] } ) => { if ( socketData[ data.roomName ] ) { if ( socketData[ data.roomName ].roomToken === data.roomToken ) { - socketData[ data.roomName ].playlist = data.data; - io.to( data.roomName ).emit( 'playlist', data.data ); + if ( socketData[ data.roomName ].playlist !== data.data ) { + socketData[ data.roomName ].playlist = data.data; + io.to( data.roomName ).emit( 'playlist', data.data ); + } } } } ); - socket.on( 'playback', ( data: { roomName: string, roomToken: string, data: boolean } ) => { + socket.on( 'playback-update', ( data: { roomName: string, roomToken: string, data: boolean } ) => { if ( socketData[ data.roomName ] ) { if ( socketData[ data.roomName ].roomToken === data.roomToken ) { socketData[ data.roomName ].playbackStatus = data.data; @@ -137,7 +139,7 @@ const run = () => { } } ); - socket.on( 'playlist-index', ( data: { roomName: string, roomToken: string, data: number } ) => { + socket.on( 'playlist-index-update', ( data: { roomName: string, roomToken: string, data: number } ) => { if ( socketData[ data.roomName ] ) { if ( socketData[ data.roomName ].roomToken === data.roomToken ) { socketData[ data.roomName ].playlistIndex = data.data; @@ -146,7 +148,7 @@ const run = () => { } } ); - socket.on( 'playback-start', ( data: { roomName: string, roomToken: string, data: number } ) => { + socket.on( 'playback-start-update', ( data: { roomName: string, roomToken: string, data: number } ) => { if ( socketData[ data.roomName ] ) { if ( socketData[ data.roomName ].roomToken === data.roomToken ) { socketData[ data.roomName ].playbackStart = data.data; @@ -174,10 +176,15 @@ const run = () => { playlistIndex: 0, roomName: roomName, roomToken: roomToken, + ownerUID: sdk.getUserData( request ).uid, }; response.send( roomToken ); } else { - response.status( 409 ).send( 'ERR_CONFLICT' ); + if ( socketData[ roomName ].ownerUID === sdk.getUserData( request ).uid ) { + response.send( socketData[ roomName ].roomToken ); + } else { + response.status( 409 ).send( 'ERR_CONFLICT' ); + } } } else { response.status( 403 ).send( 'ERR_FORBIDDEN' ); @@ -203,6 +210,8 @@ const run = () => { res.send( jwtToken ); } ); + // TODO: Get user's subscriptions using store sdk + app.use( ( request: express.Request, response: express.Response, next: express.NextFunction ) => { response.status( 404 ).send( 'ERR_NOT_FOUND' ); // response.sendFile( path.join( __dirname + '' ) ) diff --git a/backend/src/definitions.d.ts b/backend/src/definitions.d.ts index 4402200..b448c2e 100644 --- a/backend/src/definitions.d.ts +++ b/backend/src/definitions.d.ts @@ -5,6 +5,7 @@ export interface Room { playlistIndex: number; roomName: string; roomToken: string; + ownerUID: string; } export interface Song {