start integrating websocket, player basically done

This commit is contained in:
2024-06-27 16:50:03 +02:00
parent 76f543eb2f
commit 1e11f1dc2e
13 changed files with 567 additions and 48 deletions

View File

@@ -141,7 +141,7 @@
#themeSelector { #themeSelector {
position: fixed; position: fixed;
top: 10px; top: 10px;
right: 10px; left: 10px;
background: none; background: none;
border: none; border: none;
color: var( --primary-color ); color: var( --primary-color );

View File

@@ -45,7 +45,9 @@
<span class="material-symbols-outlined close-fullscreen" @click="controlUI( 'hide' )">close</span> <span class="material-symbols-outlined close-fullscreen" @click="controlUI( 'hide' )">close</span>
<playlistView :playlist="playlist" class="pl-wrapper" :currently-playing="currentlyPlayingSongIndex" :is-playing="isPlaying" :pos="pos" <playlistView :playlist="playlist" class="pl-wrapper" :currently-playing="currentlyPlayingSongIndex" :is-playing="isPlaying" :pos="pos"
@control="( action ) => { control( action ) }" @play-song="( song ) => { playSong( song ) }" @control="( action ) => { control( action ) }" @play-song="( song ) => { playSong( song ) }"
@add-new-songs="( songs ) => addNewSongs( songs )" @playlist-reorder="( move ) => moveSong( move )"></playlistView> @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 )"></playlistView>
</div> </div>
<notificationsModule ref="notifications" location="bottomleft" size="bigger"></notificationsModule> <notificationsModule ref="notifications" location="bottomleft" size="bigger"></notificationsModule>
<audio src="" id="local-audio" controls="false"></audio> <audio src="" id="local-audio" controls="false"></audio>
@@ -63,6 +65,8 @@
import type { ReadFile, Song, SongMove } from '@/scripts/song'; import type { ReadFile, Song, SongMove } from '@/scripts/song';
import { parseBlob } from 'music-metadata-browser'; import { parseBlob } from 'music-metadata-browser';
import notificationsModule from './notificationsModule.vue'; import notificationsModule from './notificationsModule.vue';
import { useUserStore } from '@/stores/userStore';
import NotificationHandler from '@/scripts/notificationHandler';
const isPlaying = ref( false ); const isPlaying = ref( false );
const repeatMode = ref( '' ); const repeatMode = ref( '' );
@@ -82,6 +86,7 @@
const pos = ref( 0 ); const pos = ref( 0 );
const duration = ref( 0 ); const duration = ref( 0 );
const notifications = ref( notificationsModule ); const notifications = ref( notificationsModule );
const notificationHandler = new NotificationHandler();
const emits = defineEmits( [ 'playerStateChange' ] ); const emits = defineEmits( [ 'playerStateChange' ] );
@@ -90,6 +95,7 @@
if ( isPlaying.value ) { if ( isPlaying.value ) {
player.control( 'play' ); player.control( 'play' );
startProgressTracker(); startProgressTracker();
} else { } else {
player.control( 'pause' ); player.control( 'pause' );
stopProgressTracker(); stopProgressTracker();
@@ -364,7 +370,7 @@
const addNewSongs = async ( songs: ReadFile[] ) => { const addNewSongs = async ( songs: ReadFile[] ) => {
let n = notifications.value.createNotification( 'Analyzing new songs', 200, 'progress', 'normal' ); let n = notifications.value.createNotification( 'Analyzing new songs', 200, 'progress', 'normal' );
playlist.value = player.getPlaylist(); playlist.value = player.getQueue();
for ( let element in songs ) { for ( let element in songs ) {
try { try {
playlist.value.push( await fetchSongData( songs[ element ] ) ); playlist.value.push( await fetchSongData( songs[ element ] ) );
@@ -374,29 +380,48 @@
notifications.value.updateNotification( n, `Analyzing new songs (${element}/${songs.length})` ); notifications.value.updateNotification( n, `Analyzing new songs (${element}/${songs.length})` );
} }
player.setPlaylist( playlist.value ); player.setPlaylist( playlist.value );
player.prepare( 0 ); if ( !isPlaying.value ) {
isPlaying.value = true; player.prepare( 0 );
setTimeout( () => { isPlaying.value = true;
startProgressTracker(); setTimeout( () => {
getDetails(); startProgressTracker();
}, 2000 ); getDetails();
}, 2000 );
}
notifications.value.cancelNotification( n ); notifications.value.cancelNotification( n );
notifications.value.createNotification( 'New songs added', 10, 'ok', 'normal' ); 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' ); emits( 'playerStateChange', isShowingFullScreenPlayer.value ? 'show' : 'hide' );
const userStore = useUserStore();
document.addEventListener( 'keydown', ( e ) => { document.addEventListener( 'keydown', ( e ) => {
if ( e.key === ' ' ) { if ( !userStore.isUsingKeyboard ) {
// TODO: fix if ( e.key === ' ' ) {
e.preventDefault(); e.preventDefault();
playPause(); playPause();
} else if ( e.key === 'ArrowRight' ) { } else if ( e.key === 'ArrowRight' ) {
e.preventDefault(); e.preventDefault();
control( 'next' ); control( 'next' );
} else if ( e.key === 'ArrowLeft' ) { } else if ( e.key === 'ArrowLeft' ) {
e.preventDefault(); e.preventDefault();
control( 'previous' ); control( 'previous' );
}
} }
} ); } );

View File

@@ -1,11 +1,11 @@
<template> <template>
<div> <div>
<h1>Playlist</h1> <h1>Playlist</h1>
<input type="file" multiple accept="audio/*" id="more-songs"> <input type="file" multiple accept="audio/*" id="more-songs" class="small-buttons">
<button @click="addNewSongs()">Load local songs</button> <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>
<p v-if="!hasSelectedSongs">Please select at least one song to proceed</p> <p v-if="!hasSelectedSongs">Please select at least one song to proceed</p>
<div class="playlist-box" id="pl-box"> <div class="playlist-box" id="pl-box">
<!-- TODO: Allow sorting -->
<!-- TODO: Allow adding more songs with search on Apple Music or loading from local disk --> <!-- TODO: Allow adding more songs with search on Apple Music or loading from local disk -->
<div class="song" v-for="song in computedPlaylist" v-bind:key="song.id" <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' ) :class="( song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) && isPlaying ? 'playing' : ' not-playing' )
@@ -27,13 +27,16 @@
<p class="playing-in">{{ getTimeUntil( song ) }}</p> <p class="playing-in">{{ getTimeUntil( song ) }}</p>
</div> </div>
</div> </div>
<searchView ref="search" @selected-song="( song ) => { addNewSongsAppleMusic( song ) }"></searchView>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { ReadFile, Song } from '@/scripts/song'; import type { AppleMusicSongData, ReadFile, Song } from '@/scripts/song';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import searchView from './searchView.vue';
const search = ref( searchView );
const props = defineProps( { const props = defineProps( {
'playlist': { 'playlist': {
default: [], default: [],
@@ -54,6 +57,11 @@
default: 0, default: 0,
required: false, required: false,
type: Number, type: Number,
},
'isLoggedIntoAppleMusic': {
default: false,
required: true,
type: Boolean,
} }
} ); } );
const hasSelectedSongs = ref( true ); const hasSelectedSongs = ref( true );
@@ -67,6 +75,12 @@
return pl; return pl;
} ); } );
const openSearch = () => {
if ( search.value ) {
search.value.controlSearch( 'show' );
}
}
const canBeMoved = computed( () => { const canBeMoved = computed( () => {
return ( direction: movementDirection, songID: string ): boolean => { return ( direction: movementDirection, songID: string ): boolean => {
let id = 0; let id = 0;
@@ -125,9 +139,8 @@
} }
const addNewSongs = () => { const addNewSongs = () => {
// TODO: Also allow loading Apple Music songs
const fileURLList: ReadFile[] = []; const fileURLList: ReadFile[] = [];
const allFiles = ( document.getElementById( 'pl-loader' ) as HTMLInputElement ).files ?? []; const allFiles = ( document.getElementById( 'more-songs' ) as HTMLInputElement ).files ?? [];
if ( allFiles.length > 0 ) { if ( allFiles.length > 0 ) {
hasSelectedSongs.value = true; hasSelectedSongs.value = true;
for ( let file = 0; file < allFiles.length; file++ ) { for ( let file = 0; file < allFiles.length; file++ ) {
@@ -139,6 +152,18 @@
} }
} }
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
}
emits( 'add-new-songs-apple-music', song );
}
type movementDirection = 'up' | 'down'; type movementDirection = 'up' | 'down';
const moveSong = ( songID: string, direction: movementDirection ) => { const moveSong = ( songID: string, direction: movementDirection ) => {
let newSongPos = 0; let newSongPos = 0;
@@ -156,7 +181,7 @@
} }
} }
const emits = defineEmits( [ 'play-song', 'control', 'playlist-reorder', 'add-new-songs' ] ); const emits = defineEmits( [ 'play-song', 'control', 'playlist-reorder', 'add-new-songs', 'add-new-songs-apple-music' ] );
</script> </script>
<style scoped> <style scoped>
@@ -294,4 +319,22 @@
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
} }
.small-buttons {
margin-bottom: 10px;
font-size: 1rem;
background: none;
border: none;
cursor: pointer;
}
.small-buttons .material-symbols-outlined {
font-size: 1.5rem;
color: var( --primary-color );
transition: all 0.5s;
}
.small-buttons:hover .material-symbols-outlined {
transform: scale(1.1);
}
</style> </style>

View File

@@ -7,8 +7,8 @@
</div> </div>
<div v-else-if="!$props.isLoggedIn"> <div v-else-if="!$props.isLoggedIn">
<p>You are not logged into Apple Music.</p> <p>You are not logged into Apple Music.</p>
<input class="fancy-button" type="file" multiple="true" accept="audio/*" id="pl-loader"><br> <input class="pl-loader-button" type="file" multiple="true" accept="audio/*" id="pl-loader"><br>
<button @click="loadPlaylistFromDisk()" class="fancy-button">Load custom playlist from disk</button> <button @click="loadPlaylistFromDisk()" class="pl-loader-button">Load custom playlist from disk</button>
<p v-if="!hasSelectedSongs">Please select at least one song to proceed!</p> <p v-if="!hasSelectedSongs">Please select at least one song to proceed!</p>
</div> </div>
<div class="playlist-wrapper"> <div class="playlist-wrapper">
@@ -84,4 +84,8 @@
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
} }
.pl-loader-button {
background-color: white;
}
</style> </style>

View File

@@ -0,0 +1,265 @@
<template>
<div>
<div id="search-bar" :class="showsSearch ? 'search-shown' : ''">
<div id="search-box-wrapper">
<input type="text" v-model="searchText" id="search-box" placeholder="Type to search..." @keyup="e => { keyHandler( e ) }">
<div class="symbol-wrapper" id="search-symbol-wrapper">
<span class="material-symbols-outlined search-symbol" @click="search()">search</span>
</div>
<div :class="'search-result-wrapper' + ( searchText.length > 0 ? ' show-search-results' : '' )">
<div v-for="result in searchResults" v-bind:key="result.id"
:class="'search-result' + ( selectedProduct === result.id ? ' prod-selected' : '' )"
@mouseenter="removeSelection()" @click="select( result )">
<div :style="'background-image: url(' + result.attributes.artwork.url.replace( '{w}', '500' ).replace( '{h}', '500' ) + ');'" class="search-product-image"></div>
<div class="search-product-name"><p><b>{{ result.attributes.name }}</b> <i>by {{ result.attributes.artistName }}</i></p></div>
</div>
<div v-if="searchResults.length === 0 && searchText.length < 3">
<p>No results to show</p>
</div>
<div v-else-if="searchText.length < 3">
<p>Enter at least three characters to start searching</p>
</div>
</div>
</div>
<div class="symbol-wrapper">
<span class="material-symbols-outlined search-symbol" @click="controlSearch( 'hide' )" id="close-icon">close</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import MusicKitJSWrapper from '@/scripts/music-player';
import type { AppleMusicSongData } from '@/scripts/song';
import { useUserStore } from '@/stores/userStore';
import { ref, type Ref } from 'vue';
const showsSearch = ref( false );
const searchText = ref( '' );
const selectedProduct = ref( '' );
let selectedProductIndex = -1;
const player = new MusicKitJSWrapper();
const updateSearchResults = () => {
if ( searchText.value.length > 2 ) {
player.findSongOnAppleMusic( searchText.value ).then( data => {
searchResults.value = data.data.results.songs.data ?? [];
selectedProductIndex = -1;
selectedProduct.value = '';
} );
}
}
const searchResults: Ref<AppleMusicSongData[]> = ref( [] );
const userStore = useUserStore();
const controlSearch = ( action: string ) => {
if ( action === 'show' ) {
userStore.setKeyboardUsageStatus( true );
showsSearch.value = true;
setTimeout( () => {
const searchBox = document.getElementById( 'search-box' ) as HTMLInputElement;
searchBox.focus();
}, 500 );
} else if ( action === 'hide' ) {
userStore.setKeyboardUsageStatus( false );
showsSearch.value = false;
}
searchText.value = '';
removeSelection();
}
const removeSelection = () => {
selectedProduct.value = '';
selectedProductIndex = -1;
}
const keyHandler = ( e: KeyboardEvent ) => {
if ( e.key === 'Escape' ) {
controlSearch( 'hide' );
} else if ( e.key === 'Enter' ) {
e.preventDefault();
if ( selectedProductIndex >= 0 ) {
select( searchResults.value[ selectedProductIndex ] );
controlSearch( 'hide' );
} else {
search();
}
} else if ( e.key === 'ArrowDown' ) {
e.preventDefault();
if ( selectedProductIndex < searchResults.value.length - 1 ) {
selectedProductIndex += 1;
selectedProduct.value = searchResults.value[ selectedProductIndex ].id;
}
} else if ( e.key === 'ArrowUp' ) {
e.preventDefault();
if ( selectedProductIndex > 0 ) {
selectedProductIndex -= 1;
selectedProduct.value = searchResults.value[ selectedProductIndex ].id;
} else {
removeSelection();
}
} else {
updateSearchResults();
}
}
const select = ( song: AppleMusicSongData ) => {
emits( 'selected-song', song );
controlSearch( 'hide' );
}
const search = () => {
emits( 'selected-song', searchResults.value[ 0 ] );
controlSearch( 'hide' );
}
const emits = defineEmits( [ 'selected-song' ] );
defineExpose( {
controlSearch
} );
</script>
<style scoped>
#search-bar {
position: fixed;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
top: -15vh;
background-color: var( --accent-background );
height: 15vh;
width: 100vw;
left: 0;
transition: all 1s;
z-index: 2;
}
#search-bar.search-shown {
top: 0;
}
#search-box {
width: calc(100% - 20px);
padding: 10px;
font-size: 150%;
}
#search-box-wrapper {
width: 60vw;
position: relative;
display: block;
}
@media only screen and (min-width: 1000px) {
#search-box-wrapper {
width: 45vw;
}
}
@media only screen and (min-width: 1500px) {
#search-box-wrapper {
width: 30vw;
}
}
#search-symbol-wrapper {
position: absolute;
right: 10px;
top: 3px;
}
.symbol-wrapper {
display: flex;
height: 3rem;
width: 3rem;
align-items: center;
justify-content: center;
}
.search-symbol {
color: black;
padding: 0;
margin: 0;
font-size: 200%;
cursor: pointer;
transition: all 0.5s;
}
.search-symbol:hover {
font-size: 250%;
}
#close-icon {
margin-left: 5px;
color: var( --primary-color );
}
</style>
<style scoped>
.search-result-wrapper {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
padding: 2px;
padding-bottom: 5px;
left: 0;
text-decoration: none;
background-color: white;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
transform-origin: top;
transform: scaleY( 0 );
transition: all 0.5s;
color: black;
}
.show-search-results {
transform: scaleY( 1 );
}
.search-result {
padding: 3px;
display: flex;
flex-direction: row;
width: 98%;
text-decoration: none;
transition: all 0.5s;
cursor: pointer;
}
.search-result:hover, .prod-selected {
text-decoration: underline black;
}
.search-product-image {
height: 4rem;
width: 4rem;
background-position: center;
background-size: cover;
margin-right: 20px;
display: flex;
align-items: center;
flex-direction: row;
justify-content: center;
}
.search-product-name {
display: flex;
flex-direction: row;
align-items: center;
text-decoration: none;
color: black;
font-weight: bold;
padding: 0;
margin: 0;
width: calc( 100% - 4rem - 20px );
}
</style>

View File

@@ -228,6 +228,7 @@ class MusicKitJSWrapper {
switch ( action ) { switch ( action ) {
case "play": case "play":
if ( this.isPreparedToPlay ) { if ( this.isPreparedToPlay ) {
this.control( 'pause' );
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) { if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
this.musicKit.play(); this.musicKit.play();
return false; return false;
@@ -285,6 +286,7 @@ class MusicKitJSWrapper {
return false; return false;
} }
case "next": case "next":
this.control( 'pause' );
if ( this.queuePos < this.queue.length - 1 ) { if ( this.queuePos < this.queue.length - 1 ) {
this.queuePos += 1; this.queuePos += 1;
this.prepare( this.queue[ this.queuePos ] ); this.prepare( this.queue[ this.queuePos ] );
@@ -300,6 +302,7 @@ class MusicKitJSWrapper {
return true; return true;
} }
case "previous": case "previous":
this.control( 'pause' );
if ( this.queuePos > 0 ) { if ( this.queuePos > 0 ) {
this.queuePos -= 1; this.queuePos -= 1;
this.prepare( this.queue[ this.queuePos ] ); this.prepare( this.queue[ this.queuePos ] );

View File

@@ -15,7 +15,25 @@ class NotificationHandler {
socket: Socket; socket: Socket;
constructor () { constructor () {
this.socket = io(); this.socket = io( localStorage.getItem( 'url' ) ?? '', {
autoConnect: false,
} );
}
/**
* Create a room token and connect to
* @param {string} roomName
* @returns {Promise<string>}
*/
connect ( roomName: string ): Promise<string> {
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 { disconnect (): void {
this.socket.disconnect(); this.socket.disconnect();
} }
joinRoom ( roomName: string ): void {
// this.socket.
}
} }
export default NotificationHandler; export default NotificationHandler;

View File

@@ -42,6 +42,14 @@ export interface Song {
additionalInfo?: string; additionalInfo?: string;
} }
export interface SongTransmitted {
title: string;
artist: string;
duration: number;
cover: string;
additionalInfo?: string;
}
export interface ReadFile { export interface ReadFile {
url: string; url: string;

View File

@@ -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 }
})

View File

@@ -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;
}
}
} );

View File

@@ -5,6 +5,10 @@ import jwt from 'jsonwebtoken';
import cors from 'cors'; import cors from 'cors';
import account from './account'; import account from './account';
import sdk from 'oauth-janishutz-client-server'; 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 declare let __dirname: string | undefined
if ( typeof( __dirname ) === 'undefined' ) { if ( typeof( __dirname ) === 'undefined' ) {
@@ -22,6 +26,8 @@ const run = () => {
origin: true origin: true
} ) ); } ) );
const httpServer = createServer( app );
// Load id.janishutz.com SDK and allow signing in // Load id.janishutz.com SDK and allow signing in
sdk.routes( app, ( uid: string ) => { sdk.routes( app, ( uid: string ) => {
return new Promise( ( resolve, reject ) => { return new Promise( ( resolve, reject ) => {
@@ -42,11 +48,126 @@ const run = () => {
} ); } );
}, sdkConfig ); }, 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 <a href="https://music.janishutz.com">https://music.janishutz.com</a> to use this service' ); response.send( 'Please visit <a href="https://music.janishutz.com">https://music.janishutz.com</a> 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 ) => { app.get( '/getAppleMusicDevToken', ( req, res ) => {
// sign dev token // sign dev token
@@ -73,7 +194,7 @@ const run = () => {
const PORT = process.env.PORT || 8081; const PORT = process.env.PORT || 8081;
app.listen( PORT ); httpServer.listen( PORT );
} }
export default { export default {

16
backend/src/definitions.d.ts vendored Normal file
View File

@@ -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;
}

View File

@@ -131,7 +131,7 @@ export default {
<style scoped> <style scoped>
.message-box { .message-box {
position: fixed; position: fixed;
z-index: -1; z-index: -20;
color: white; color: white;
transition: all 0.5s; transition: all 0.5s;
width: 95vw; width: 95vw;