From 1e11f1dc2ee9223fbc4f35ded357e38d056e682b Mon Sep 17 00:00:00 2001 From: Janis Hutz Date: Thu, 27 Jun 2024 16:50:03 +0200 Subject: [PATCH] start integrating websocket, player basically done --- MusicPlayerV2-GUI/src/App.vue | 2 +- .../src/components/playerView.vue | 61 ++-- .../src/components/playlistView.vue | 57 +++- .../src/components/playlistsView.vue | 8 +- .../src/components/searchView.vue | 265 ++++++++++++++++++ MusicPlayerV2-GUI/src/scripts/music-player.ts | 3 + .../src/scripts/notificationHandler.ts | 24 +- MusicPlayerV2-GUI/src/scripts/song.d.ts | 8 + MusicPlayerV2-GUI/src/stores/counter.ts | 12 - MusicPlayerV2-GUI/src/stores/userStore.ts | 32 +++ backend/src/app.ts | 125 ++++++++- backend/src/definitions.d.ts | 16 ++ old/frontend/src/components/notifications.vue | 2 +- 13 files changed, 567 insertions(+), 48 deletions(-) create mode 100644 MusicPlayerV2-GUI/src/components/searchView.vue delete mode 100644 MusicPlayerV2-GUI/src/stores/counter.ts create mode 100644 MusicPlayerV2-GUI/src/stores/userStore.ts create mode 100644 backend/src/definitions.d.ts diff --git a/MusicPlayerV2-GUI/src/App.vue b/MusicPlayerV2-GUI/src/App.vue index c1c2498..0ccb3fd 100644 --- a/MusicPlayerV2-GUI/src/App.vue +++ b/MusicPlayerV2-GUI/src/App.vue @@ -141,7 +141,7 @@ #themeSelector { position: fixed; top: 10px; - right: 10px; + left: 10px; background: none; border: none; color: var( --primary-color ); diff --git a/MusicPlayerV2-GUI/src/components/playerView.vue b/MusicPlayerV2-GUI/src/components/playerView.vue index b495a1f..dde102a 100644 --- a/MusicPlayerV2-GUI/src/components/playerView.vue +++ b/MusicPlayerV2-GUI/src/components/playerView.vue @@ -45,7 +45,9 @@ close + @add-new-songs="( songs ) => addNewSongs( songs )" @playlist-reorder="( move ) => moveSong( move )" + :is-logged-into-apple-music="player.isLoggedIn" + @add-new-songs-apple-music="( song ) => addNewSongFromObject( song )"> @@ -63,6 +65,8 @@ import type { ReadFile, Song, SongMove } from '@/scripts/song'; import { parseBlob } from 'music-metadata-browser'; import notificationsModule from './notificationsModule.vue'; + import { useUserStore } from '@/stores/userStore'; + import NotificationHandler from '@/scripts/notificationHandler'; const isPlaying = ref( false ); const repeatMode = ref( '' ); @@ -82,6 +86,7 @@ const pos = ref( 0 ); const duration = ref( 0 ); const notifications = ref( notificationsModule ); + const notificationHandler = new NotificationHandler(); const emits = defineEmits( [ 'playerStateChange' ] ); @@ -90,6 +95,7 @@ if ( isPlaying.value ) { player.control( 'play' ); startProgressTracker(); + } else { player.control( 'pause' ); stopProgressTracker(); @@ -364,7 +370,7 @@ const addNewSongs = async ( songs: ReadFile[] ) => { let n = notifications.value.createNotification( 'Analyzing new songs', 200, 'progress', 'normal' ); - playlist.value = player.getPlaylist(); + playlist.value = player.getQueue(); for ( let element in songs ) { try { playlist.value.push( await fetchSongData( songs[ element ] ) ); @@ -374,29 +380,48 @@ notifications.value.updateNotification( n, `Analyzing new songs (${element}/${songs.length})` ); } player.setPlaylist( playlist.value ); - player.prepare( 0 ); - isPlaying.value = true; - setTimeout( () => { - startProgressTracker(); - getDetails(); - }, 2000 ); + if ( !isPlaying.value ) { + player.prepare( 0 ); + isPlaying.value = true; + setTimeout( () => { + startProgressTracker(); + getDetails(); + }, 2000 ); + } notifications.value.cancelNotification( n ); notifications.value.createNotification( 'New songs added', 10, 'ok', 'normal' ); } + const addNewSongFromObject = ( song: Song ) => { + playlist.value = player.getQueue(); + playlist.value.push( song ); + player.setPlaylist( playlist.value ); + if ( !isPlaying.value ) { + player.prepare( 0 ); + isPlaying.value = true; + setTimeout( () => { + startProgressTracker(); + getDetails(); + }, 2000 ); + } + } + emits( 'playerStateChange', isShowingFullScreenPlayer.value ? 'show' : 'hide' ); + const userStore = useUserStore(); + document.addEventListener( 'keydown', ( e ) => { - if ( e.key === ' ' ) { - // TODO: fix - e.preventDefault(); - playPause(); - } else if ( e.key === 'ArrowRight' ) { - e.preventDefault(); - control( 'next' ); - } else if ( e.key === 'ArrowLeft' ) { - e.preventDefault(); - control( 'previous' ); + if ( !userStore.isUsingKeyboard ) { + if ( e.key === ' ' ) { + e.preventDefault(); + playPause(); + } else if ( e.key === 'ArrowRight' ) { + e.preventDefault(); + control( 'next' ); + } else if ( e.key === 'ArrowLeft' ) { + e.preventDefault(); + control( 'previous' ); + } } } ); diff --git a/MusicPlayerV2-GUI/src/components/playlistView.vue b/MusicPlayerV2-GUI/src/components/playlistView.vue index 0cca5fd..834f76f 100644 --- a/MusicPlayerV2-GUI/src/components/playlistView.vue +++ b/MusicPlayerV2-GUI/src/components/playlistView.vue @@ -1,11 +1,11 @@ \ No newline at end of file diff --git a/MusicPlayerV2-GUI/src/components/playlistsView.vue b/MusicPlayerV2-GUI/src/components/playlistsView.vue index c17635a..46fa4be 100644 --- a/MusicPlayerV2-GUI/src/components/playlistsView.vue +++ b/MusicPlayerV2-GUI/src/components/playlistsView.vue @@ -7,8 +7,8 @@

You are not logged into Apple Music.

-
- +
+

Please select at least one song to proceed!

@@ -84,4 +84,8 @@ cursor: pointer; user-select: none; } + + .pl-loader-button { + background-color: white; + } \ No newline at end of file diff --git a/MusicPlayerV2-GUI/src/components/searchView.vue b/MusicPlayerV2-GUI/src/components/searchView.vue new file mode 100644 index 0000000..b7ff761 --- /dev/null +++ b/MusicPlayerV2-GUI/src/components/searchView.vue @@ -0,0 +1,265 @@ + + + + + + + + \ No newline at end of file diff --git a/MusicPlayerV2-GUI/src/scripts/music-player.ts b/MusicPlayerV2-GUI/src/scripts/music-player.ts index 20f743b..4c0e091 100644 --- a/MusicPlayerV2-GUI/src/scripts/music-player.ts +++ b/MusicPlayerV2-GUI/src/scripts/music-player.ts @@ -228,6 +228,7 @@ class MusicKitJSWrapper { switch ( action ) { case "play": if ( this.isPreparedToPlay ) { + this.control( 'pause' ); if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) { this.musicKit.play(); return false; @@ -285,6 +286,7 @@ class MusicKitJSWrapper { return false; } case "next": + this.control( 'pause' ); if ( this.queuePos < this.queue.length - 1 ) { this.queuePos += 1; this.prepare( this.queue[ this.queuePos ] ); @@ -300,6 +302,7 @@ class MusicKitJSWrapper { return true; } case "previous": + this.control( 'pause' ); if ( this.queuePos > 0 ) { this.queuePos -= 1; this.prepare( this.queue[ this.queuePos ] ); diff --git a/MusicPlayerV2-GUI/src/scripts/notificationHandler.ts b/MusicPlayerV2-GUI/src/scripts/notificationHandler.ts index 5b45f1c..25c5d20 100644 --- a/MusicPlayerV2-GUI/src/scripts/notificationHandler.ts +++ b/MusicPlayerV2-GUI/src/scripts/notificationHandler.ts @@ -15,7 +15,25 @@ class NotificationHandler { socket: Socket; constructor () { - this.socket = io(); + this.socket = io( localStorage.getItem( 'url' ) ?? '', { + autoConnect: false, + } ); + } + + /** + * Create a room token and connect to + * @param {string} roomName + * @returns {Promise} + */ + connect ( roomName: string ): Promise { + fetch( localStorage.getItem( 'url' ) + '/createRoomToken', { credentials: 'include' } ).then( res => { + if ( res.status === 200 ) { + res.json().then( json => { + + } ); + } + } ); + } /** @@ -35,10 +53,6 @@ class NotificationHandler { disconnect (): void { this.socket.disconnect(); } - - joinRoom ( roomName: string ): void { - // this.socket. - } } export default NotificationHandler; \ No newline at end of file diff --git a/MusicPlayerV2-GUI/src/scripts/song.d.ts b/MusicPlayerV2-GUI/src/scripts/song.d.ts index 9fde4b5..1c01058 100644 --- a/MusicPlayerV2-GUI/src/scripts/song.d.ts +++ b/MusicPlayerV2-GUI/src/scripts/song.d.ts @@ -42,6 +42,14 @@ export interface Song { additionalInfo?: string; } +export interface SongTransmitted { + title: string; + artist: string; + duration: number; + cover: string; + additionalInfo?: string; +} + export interface ReadFile { url: string; diff --git a/MusicPlayerV2-GUI/src/stores/counter.ts b/MusicPlayerV2-GUI/src/stores/counter.ts deleted file mode 100644 index b6757ba..0000000 --- a/MusicPlayerV2-GUI/src/stores/counter.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ref, computed } from 'vue' -import { defineStore } from 'pinia' - -export const useCounterStore = defineStore('counter', () => { - const count = ref(0) - const doubleCount = computed(() => count.value * 2) - function increment() { - count.value++ - } - - return { count, doubleCount, increment } -}) diff --git a/MusicPlayerV2-GUI/src/stores/userStore.ts b/MusicPlayerV2-GUI/src/stores/userStore.ts new file mode 100644 index 0000000..7733039 --- /dev/null +++ b/MusicPlayerV2-GUI/src/stores/userStore.ts @@ -0,0 +1,32 @@ +/* +* LanguageSchoolHossegorBookingSystem - userStore.js +* +* Created by Janis Hutz 10/27/2023, Licensed under a proprietary License +* https://janishutz.com, development@janishutz.com +* +* +*/ + +import { defineStore } from 'pinia'; + +export const useUserStore = defineStore( 'user', { + state: () => ( { 'isUserAuth': false, 'isAdminAuth': false, 'isUsingKeyboard': false, 'username': '' } ), + getters: { + getUserAuthenticated: ( state ) => state.isUserAuth, + getAdminAuthenticated: ( state ) => state.isAdminAuth, + }, + actions: { + setUserAuth ( auth: boolean ) { + this.isUserAuth = auth; + }, + setAdminAuth ( auth: boolean ) { + this.isAdminAuth = auth; + }, + setUsername ( username: string ) { + this.username = username; + }, + setKeyboardUsageStatus ( status: boolean ) { + this.isUsingKeyboard = status; + } + } +} ); \ No newline at end of file diff --git a/backend/src/app.ts b/backend/src/app.ts index bccc914..93e7559 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -5,6 +5,10 @@ import jwt from 'jsonwebtoken'; import cors from 'cors'; import account from './account'; import sdk from 'oauth-janishutz-client-server'; +import { createServer } from 'node:http'; +import { Server } from 'socket.io'; +import crypto from 'node:crypto'; +import type { Room, Song } from './definitions'; declare let __dirname: string | undefined if ( typeof( __dirname ) === 'undefined' ) { @@ -22,6 +26,8 @@ const run = () => { origin: true } ) ); + const httpServer = createServer( app ); + // Load id.janishutz.com SDK and allow signing in sdk.routes( app, ( uid: string ) => { return new Promise( ( resolve, reject ) => { @@ -42,11 +48,126 @@ const run = () => { } ); }, sdkConfig ); + // Websocket for events + interface SocketData { + [key: string]: Room; + } + const socketData: SocketData = {}; + const io = new Server( httpServer, { + cors: { + origin: true, + credentials: true, + } + } ); - app.get( '/', ( request, response ) => { + io.on( 'connection', ( socket ) => { + socket.on( 'create-room', ( room: { name: string, token: string }, cb: ( res: { status: boolean, msg: string } ) => void ) => { + if ( room.token === socketData[ room.name ].roomToken ) { + socket.join( room.name ); + cb( { + status: true, + msg: 'ADDED_TO_ROOM' + } ) + } else { + cb( { + status: false, + msg: 'ERR_TOKEN_INVALID' + } ); + } + } ); + + socket.on( 'join-room', ( room: string, cb: ( res: { status: boolean, msg: string, data?: { playbackStatus: boolean, playbackStart: number, playlist: Song[], playlistIndex: number } } ) => void ) => { + if ( socketData[ room ] ) { + socket.join( room ); + cb( { + data: { + playbackStart: socketData[ room ].playbackStart, + playbackStatus: socketData[ room ].playbackStatus, + playlist: socketData[ room ].playlist, + playlistIndex: socketData[ room ].playlistIndex, + }, + msg: 'STATUS_OK', + status: true, + } ) + } else { + cb( { + msg: 'ERR_NO_ROOM_WITH_THIS_ID', + status: false, + } ) + } + } ); + + socket.on( 'tampering', ( data: { msg: string, roomName: string } ) => { + if ( data.roomName ) { + socket.to( data.roomName ).emit( 'tampering-msg', data.msg ); + } + } ) + + socket.on( 'playlist', ( 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 ); + } + } + } ); + + socket.on( 'playback', ( data: { roomName: string, roomToken: string, data: boolean } ) => { + if ( socketData[ data.roomName ] ) { + if ( socketData[ data.roomName ].roomToken === data.roomToken ) { + socketData[ data.roomName ].playbackStatus = data.data; + io.to( data.roomName ).emit( 'playback', data.data ); + } + } + } ); + + socket.on( 'playlist-index', ( data: { roomName: string, roomToken: string, data: number } ) => { + if ( socketData[ data.roomName ] ) { + if ( socketData[ data.roomName ].roomToken === data.roomToken ) { + socketData[ data.roomName ].playlistIndex = data.data; + io.to( data.roomName ).emit( 'playlist-index', data.data ); + } + } + } ); + + socket.on( 'playback-start', ( data: { roomName: string, roomToken: string, data: number } ) => { + if ( socketData[ data.roomName ] ) { + if ( socketData[ data.roomName ].roomToken === data.roomToken ) { + socketData[ data.roomName ].playbackStart = data.data; + io.to( data.roomName ).emit( 'playback-start', data.data ); + } + } + } ); + } ); + + + app.get( '/', ( request: express.Request, response: express.Response ) => { response.send( 'Please visit https://music.janishutz.com to use this service' ); } ); + + app.get( '/createRoomToken', ( request: express.Request, response: express.Response ) => { + if ( sdk.checkAuth( request ) ) { + const roomName = String( request.query.roomName ) ?? ''; + if ( !socketData[ roomName ] ) { + const roomToken = crypto.randomUUID(); + socketData[ roomName ] = { + playbackStart: 0, + playbackStatus: false, + playlist: [], + playlistIndex: 0, + roomName: roomName, + roomToken: roomToken, + }; + response.send( roomToken ); + } else { + response.status( 409 ).send( 'ERR_CONFLICT' ); + } + } else { + response.status( 403 ).send( 'ERR_FORBIDDEN' ); + } + } ); + app.get( '/getAppleMusicDevToken', ( req, res ) => { // sign dev token @@ -73,7 +194,7 @@ const run = () => { const PORT = process.env.PORT || 8081; - app.listen( PORT ); + httpServer.listen( PORT ); } export default { diff --git a/backend/src/definitions.d.ts b/backend/src/definitions.d.ts new file mode 100644 index 0000000..4402200 --- /dev/null +++ b/backend/src/definitions.d.ts @@ -0,0 +1,16 @@ +export interface Room { + playbackStatus: boolean; + playbackStart: number; + playlist: Song[]; + playlistIndex: number; + roomName: string; + roomToken: string; +} + +export interface Song { + title: string; + artist: string; + duration: number; + cover: string; + additionalInfo?: string; +} \ No newline at end of file diff --git a/old/frontend/src/components/notifications.vue b/old/frontend/src/components/notifications.vue index 5b74c58..58dd47b 100644 --- a/old/frontend/src/components/notifications.vue +++ b/old/frontend/src/components/notifications.vue @@ -131,7 +131,7 @@ export default {