mirror of
https://github.com/janishutz/MusicPlayerV2.git
synced 2025-11-26 05:14:24 +00:00
Prepare for new sdk
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import ColorThief from 'colorthief';
|
||||
|
||||
const colorThief = new ColorThief();
|
||||
|
||||
const getImageData = (): Promise<number[][]> => {
|
||||
return new Promise( ( resolve ) => {
|
||||
const img = ( document.getElementById( 'current-image' ) as HTMLImageElement );
|
||||
return new Promise( resolve => {
|
||||
const img = document.getElementById( 'current-image' ) as HTMLImageElement;
|
||||
|
||||
if ( img.complete ) {
|
||||
resolve( colorThief.getPalette( img ) );
|
||||
} else {
|
||||
@@ -12,32 +14,39 @@ const getImageData = (): Promise<number[][]> => {
|
||||
} );
|
||||
}
|
||||
} );
|
||||
}
|
||||
};
|
||||
|
||||
const createBackground = () => {
|
||||
return new Promise( ( resolve ) => {
|
||||
return new Promise( resolve => {
|
||||
getImageData().then( 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 ] ) {
|
||||
@@ -61,45 +70,56 @@ const createBackground = () => {
|
||||
outColours += colours[ i ] + ',';
|
||||
}
|
||||
}
|
||||
|
||||
outColours += colours[ 0 ] ?? 'blue' + ')';
|
||||
resolve( outColours );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
};
|
||||
|
||||
let callbackFun = () => {};
|
||||
|
||||
let callbackFun = () => {}
|
||||
const subscribeToBeatUpdate = ( cb: () => void ) => {
|
||||
callbackFun = cb;
|
||||
micAudioHandler();
|
||||
}
|
||||
};
|
||||
|
||||
const unsubscribeFromBeatUpdate = () => {
|
||||
callbackFun = () => {}
|
||||
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 ) => {
|
||||
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
|
||||
@@ -115,25 +135,27 @@ const micAudioHandler = () => {
|
||||
callbackFun();
|
||||
}
|
||||
}
|
||||
|
||||
prevSpectrum = currentSpectrum;
|
||||
}, 60 / 180 * 250 );
|
||||
} );
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
/*
|
||||
* 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";
|
||||
import type { SSEMap } from "./song";
|
||||
import {
|
||||
io, type Socket
|
||||
} from 'socket.io-client';
|
||||
import type {
|
||||
SSEMap
|
||||
} from './song';
|
||||
|
||||
class SocketConnection {
|
||||
|
||||
socket: Socket;
|
||||
|
||||
roomName: string;
|
||||
|
||||
isConnected: boolean;
|
||||
|
||||
useSocket: boolean;
|
||||
|
||||
eventSource?: EventSource;
|
||||
|
||||
toBeListenedForItems: SSEMap;
|
||||
|
||||
reconnectRetryCount: number;
|
||||
|
||||
openConnectionsCount: number;
|
||||
|
||||
constructor () {
|
||||
this.socket = io( localStorage.getItem( 'url' ) ?? '', {
|
||||
autoConnect: false,
|
||||
'autoConnect': false,
|
||||
} );
|
||||
this.roomName = location.pathname.split( '/' )[ 2 ];
|
||||
this.isConnected = false;
|
||||
@@ -35,55 +38,71 @@ class SocketConnection {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a room token and connect to
|
||||
* Create a room token and connect to
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
connect (): Promise<any> {
|
||||
connect (): Promise<unknown> {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
if ( this.reconnectRetryCount < 5 ) {
|
||||
if ( this.useSocket ) {
|
||||
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' );
|
||||
this.socket.emit(
|
||||
'join-room', this.roomName, ( res: {
|
||||
'status': boolean,
|
||||
'msg': string,
|
||||
'data': unknown
|
||||
} ) => {
|
||||
if ( res.status === true ) {
|
||||
this.isConnected = true;
|
||||
resolve( res.data );
|
||||
} else {
|
||||
console.debug( res.msg );
|
||||
reject( 'ERR_ROOM_CONNECTING' );
|
||||
}
|
||||
}
|
||||
} );
|
||||
);
|
||||
} else {
|
||||
if ( this.openConnectionsCount < 1 && !this.isConnected ) {
|
||||
this.openConnectionsCount += 1;
|
||||
fetch( localStorage.getItem( 'url' ) + '/socket/joinRoom?room=' + this.roomName, { credentials: 'include' } ).then( res => {
|
||||
fetch( localStorage.getItem( 'url' ) + '/socket/joinRoom?room=' + this.roomName, {
|
||||
'credentials': 'include'
|
||||
} ).then( res => {
|
||||
if ( res.status === 200 ) {
|
||||
this.eventSource = new EventSource( localStorage.getItem( 'url' ) + '/socket/connection?room=' + this.roomName, { withCredentials: true } );
|
||||
this.eventSource
|
||||
= new EventSource( localStorage.getItem( 'url' )
|
||||
+ '/socket/connection?room=' + this.roomName, {
|
||||
'withCredentials': true
|
||||
} );
|
||||
|
||||
this.eventSource.onopen = () => {
|
||||
this.isConnected = true;
|
||||
this.reconnectRetryCount = 0;
|
||||
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Connection successfully established!' );
|
||||
}
|
||||
console.log( '[ SSE Connection ] - '
|
||||
+ new Date().toISOString() + ': Connection successfully established!' );
|
||||
};
|
||||
|
||||
this.eventSource.onmessage = ( e ) => {
|
||||
this.eventSource.onmessage = e => {
|
||||
const d = JSON.parse( e.data );
|
||||
|
||||
if ( this.toBeListenedForItems[ d.type ] ) {
|
||||
this.toBeListenedForItems[ d.type ]( d.data );
|
||||
} else if ( d.type === 'basics' ) {
|
||||
resolve( d.data );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.eventSource.onerror = () => {
|
||||
if ( this.isConnected ) {
|
||||
this.isConnected = false;
|
||||
this.openConnectionsCount -= 1;
|
||||
this.eventSource?.close();
|
||||
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Reconnecting due to connection error!' );
|
||||
console.log( '[ SSE Connection ] - '
|
||||
+ new Date().toISOString()
|
||||
+ ': Reconnecting due to connection error!' );
|
||||
// console.debug( e );
|
||||
|
||||
|
||||
this.eventSource = undefined;
|
||||
|
||||
|
||||
this.reconnectRetryCount += 1;
|
||||
setTimeout( () => {
|
||||
this.connect();
|
||||
@@ -91,13 +110,18 @@ class SocketConnection {
|
||||
}
|
||||
};
|
||||
} else {
|
||||
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Could not connect due to error ' + res.status );
|
||||
console.log( '[ SSE Connection ] - '
|
||||
+ new Date().toISOString()
|
||||
+ ': Could not connect due to error ' + res.status );
|
||||
reject( 'ERR_ROOM_CONNECTING' );
|
||||
}
|
||||
} ).catch( () => {
|
||||
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Could not connect due to error.' );
|
||||
reject( 'ERR_ROOM_CONNECTING' );
|
||||
} );
|
||||
} )
|
||||
.catch( () => {
|
||||
console.log( '[ SSE Connection ] - '
|
||||
+ new Date().toISOString()
|
||||
+ ': Could not connect due to error.' );
|
||||
reject( 'ERR_ROOM_CONNECTING' );
|
||||
} );
|
||||
} else {
|
||||
console.log( '[ SSE Connection ]: Trimmed connections' );
|
||||
reject( 'ERR_TOO_MANY_CONNECTIONS' );
|
||||
@@ -116,16 +140,23 @@ class SocketConnection {
|
||||
* @param {any} data
|
||||
* @returns {void}
|
||||
*/
|
||||
emit ( event: string, data: any ): void {
|
||||
emit ( event: string, data: unknown ): void {
|
||||
if ( this.isConnected ) {
|
||||
if ( this.useSocket ) {
|
||||
this.socket.emit( event, { 'roomName': this.roomName, 'data': data } );
|
||||
this.socket.emit( event, {
|
||||
'roomName': this.roomName,
|
||||
'data': data
|
||||
} );
|
||||
} else {
|
||||
fetch( localStorage.getItem( 'url' ) + '/socket/update', {
|
||||
method: 'post',
|
||||
body: JSON.stringify( { 'event': event, 'roomName': this.roomName, 'data': data } ),
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'method': 'post',
|
||||
'body': JSON.stringify( {
|
||||
'event': event,
|
||||
'roomName': this.roomName,
|
||||
'data': data
|
||||
} ),
|
||||
'credentials': 'include',
|
||||
'headers': {
|
||||
'Content-Type': 'application/json',
|
||||
'charset': 'utf-8'
|
||||
}
|
||||
@@ -140,11 +171,11 @@ class SocketConnection {
|
||||
* @param {( data: any ) => void} cb The callback function / listener function
|
||||
* @returns {void}
|
||||
*/
|
||||
registerListener ( event: string, cb: ( data: any ) => void ): void {
|
||||
registerListener ( event: string, cb: ( data: unknown ) => void ): void {
|
||||
if ( this.useSocket ) {
|
||||
if ( this.isConnected ) {
|
||||
this.socket.on( event, cb );
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.toBeListenedForItems[ event ] = cb;
|
||||
}
|
||||
@@ -171,9 +202,11 @@ class SocketConnection {
|
||||
if ( this.eventSource ) {
|
||||
return this.eventSource!.OPEN && this.isConnected;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default SocketConnection;
|
||||
export default SocketConnection;
|
||||
|
||||
@@ -1,25 +1,39 @@
|
||||
import type { SearchResult, Song, SongMove } from "./song";
|
||||
import type {
|
||||
SearchResult, Song, SongMove
|
||||
} from './song';
|
||||
|
||||
interface Config {
|
||||
devToken: string;
|
||||
userToken: string;
|
||||
'devToken': string;
|
||||
'userToken': string;
|
||||
}
|
||||
|
||||
type ControlAction = 'play' | 'pause' | 'next' | 'previous' | 'skip-10' | 'back-10';
|
||||
type RepeatMode = 'off' | 'once' | 'all';
|
||||
|
||||
class MusicKitJSWrapper {
|
||||
|
||||
playingSongID: number;
|
||||
|
||||
playlist: Song[];
|
||||
|
||||
queue: number[];
|
||||
|
||||
config: Config;
|
||||
|
||||
musicKit: any;
|
||||
|
||||
isLoggedIn: boolean;
|
||||
|
||||
isPreparedToPlay: boolean;
|
||||
|
||||
repeatMode: RepeatMode;
|
||||
|
||||
isShuffleEnabled: boolean;
|
||||
|
||||
hasEncounteredAuthError: boolean;
|
||||
|
||||
queuePos: number;
|
||||
|
||||
audioPlayer: HTMLAudioElement;
|
||||
|
||||
constructor () {
|
||||
@@ -27,8 +41,8 @@ class MusicKitJSWrapper {
|
||||
this.playlist = [];
|
||||
this.queue = [];
|
||||
this.config = {
|
||||
devToken: '',
|
||||
userToken: '',
|
||||
'devToken': '',
|
||||
'userToken': '',
|
||||
};
|
||||
this.isShuffleEnabled = false;
|
||||
this.repeatMode = 'off';
|
||||
@@ -58,16 +72,18 @@ class MusicKitJSWrapper {
|
||||
this.musicKit.authorize().then( () => {
|
||||
this.isLoggedIn = true;
|
||||
this.init();
|
||||
} ).catch( () => {
|
||||
this.hasEncounteredAuthError = true;
|
||||
} );
|
||||
} )
|
||||
.catch( () => {
|
||||
this.hasEncounteredAuthError = true;
|
||||
} );
|
||||
} else {
|
||||
this.musicKit.authorize().then( () => {
|
||||
this.isLoggedIn = true;
|
||||
this.init();
|
||||
} ).catch( () => {
|
||||
this.hasEncounteredAuthError = true;
|
||||
} );
|
||||
} )
|
||||
.catch( () => {
|
||||
this.hasEncounteredAuthError = true;
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,25 +92,29 @@ class MusicKitJSWrapper {
|
||||
* @returns {void}
|
||||
*/
|
||||
init (): void {
|
||||
fetch( localStorage.getItem( 'url' ) + '/getAppleMusicDevToken', { credentials: 'include' } ).then( res => {
|
||||
fetch( localStorage.getItem( 'url' ) + '/getAppleMusicDevToken', {
|
||||
'credentials': 'include'
|
||||
} ).then( res => {
|
||||
if ( res.status === 200 ) {
|
||||
res.text().then( token => {
|
||||
this.audioPlayer = document.getElementById( 'local-audio' ) as HTMLAudioElement;
|
||||
// MusicKit global is now defined
|
||||
MusicKit.configure( {
|
||||
developerToken: token,
|
||||
app: {
|
||||
name: 'MusicPlayer',
|
||||
build: '3'
|
||||
'developerToken': token,
|
||||
'app': {
|
||||
'name': 'MusicPlayer',
|
||||
'build': '3'
|
||||
},
|
||||
storefrontId: 'CH',
|
||||
'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;
|
||||
} );
|
||||
} );
|
||||
@@ -107,7 +127,10 @@ class MusicKitJSWrapper {
|
||||
* @returns {boolean[]} Returns an array, where the first element indicates login status, the second one, if an error was encountered
|
||||
*/
|
||||
getAuth (): boolean[] {
|
||||
return [ this.isLoggedIn, this.hasEncounteredAuthError ];
|
||||
return [
|
||||
this.isLoggedIn,
|
||||
this.hasEncounteredAuthError
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,8 +142,8 @@ class MusicKitJSWrapper {
|
||||
apiGetRequest ( url: string, callback: ( data: object ) => void ): void {
|
||||
if ( this.config.devToken != '' && this.config.userToken != '' ) {
|
||||
fetch( url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'method': 'GET',
|
||||
'headers': {
|
||||
'Authorization': `Bearer ${ this.config.devToken }`,
|
||||
'Music-User-Token': this.config.userToken
|
||||
}
|
||||
@@ -128,13 +151,19 @@ class MusicKitJSWrapper {
|
||||
if ( res.status === 200 ) {
|
||||
res.json().then( json => {
|
||||
try {
|
||||
callback( { 'status': 'ok', 'data': json } );
|
||||
} catch( err ) { /* empty */}
|
||||
callback( {
|
||||
'status': 'ok',
|
||||
'data': json
|
||||
} );
|
||||
} catch ( err ) { /* empty */ }
|
||||
} );
|
||||
} else {
|
||||
try {
|
||||
callback( { 'status': 'error', 'error': res.status } );
|
||||
} catch( err ) { /* empty */}
|
||||
callback( {
|
||||
'status': 'error',
|
||||
'error': res.status
|
||||
} );
|
||||
} catch ( err ) { /* empty */ }
|
||||
}
|
||||
} );
|
||||
} else return;
|
||||
@@ -152,34 +181,41 @@ class MusicKitJSWrapper {
|
||||
|
||||
setPlaylistByID ( id: string ): Promise<void> {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
this.musicKit.setQueue( { playlist: id } ).then( () => {
|
||||
this.musicKit.setQueue( {
|
||||
'playlist': id
|
||||
} ).then( () => {
|
||||
const pl = this.musicKit.queue.items;
|
||||
const songs: Song[] = [];
|
||||
|
||||
for ( const item in pl ) {
|
||||
let url = pl[ item ].attributes.artwork.url;
|
||||
|
||||
url = url.replace( '{w}', pl[ item ].attributes.artwork.width );
|
||||
url = url.replace( '{h}', pl[ item ].attributes.artwork.height );
|
||||
const song: Song = {
|
||||
artist: pl[ item ].attributes.artistName,
|
||||
cover: url,
|
||||
duration: pl[ item ].attributes.durationInMillis / 1000,
|
||||
id: pl[ item ].id,
|
||||
origin: 'apple-music',
|
||||
title: pl[ item ].attributes.name,
|
||||
genres: pl[ item ].attributes.genreNames
|
||||
}
|
||||
'artist': pl[ item ].attributes.artistName,
|
||||
'cover': url,
|
||||
'duration': pl[ item ].attributes.durationInMillis / 1000,
|
||||
'id': pl[ item ].id,
|
||||
'origin': 'apple-music',
|
||||
'title': pl[ item ].attributes.name,
|
||||
'genres': pl[ item ].attributes.genreNames
|
||||
};
|
||||
|
||||
songs.push( song );
|
||||
}
|
||||
|
||||
this.playlist = songs;
|
||||
this.setShuffle( this.isShuffleEnabled );
|
||||
this.queuePos = 0;
|
||||
this.playingSongID = this.queue[ 0 ];
|
||||
this.prepare( this.playingSongID );
|
||||
resolve();
|
||||
} ).catch( err => {
|
||||
console.error( err );
|
||||
reject( err );
|
||||
} );
|
||||
} )
|
||||
.catch( err => {
|
||||
console.error( err );
|
||||
reject( err );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
@@ -192,20 +228,25 @@ class MusicKitJSWrapper {
|
||||
if ( this.playlist.length > 0 ) {
|
||||
this.playingSongID = playlistID;
|
||||
this.isPreparedToPlay = true;
|
||||
|
||||
for ( const el in this.queue ) {
|
||||
if ( this.queue[ el ] === playlistID ) {
|
||||
this.queuePos = parseInt( el );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||
this.musicKit.setQueue( { 'song': this.playlist[ this.playingSongID ].id } ).then( () => {
|
||||
this.musicKit.setQueue( {
|
||||
'song': this.playlist[ this.playingSongID ].id
|
||||
} ).then( () => {
|
||||
setTimeout( () => {
|
||||
this.control( 'play' );
|
||||
}, 500 );
|
||||
} ).catch( ( err ) => {
|
||||
console.log( err );
|
||||
} );
|
||||
} )
|
||||
.catch( err => {
|
||||
console.log( err );
|
||||
} );
|
||||
} else {
|
||||
this.audioPlayer = document.getElementById( 'local-audio' ) as HTMLAudioElement;
|
||||
this.audioPlayer.src = this.playlist[ this.playingSongID ].id;
|
||||
@@ -213,6 +254,7 @@ class MusicKitJSWrapper {
|
||||
this.control( 'play' );
|
||||
}, 500 );
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
@@ -226,50 +268,63 @@ class MusicKitJSWrapper {
|
||||
*/
|
||||
control ( action: ControlAction ): boolean {
|
||||
switch ( action ) {
|
||||
case "play":
|
||||
case 'play':
|
||||
if ( this.isPreparedToPlay ) {
|
||||
this.control( 'pause' );
|
||||
|
||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||
this.musicKit.play();
|
||||
|
||||
return false;
|
||||
} else {
|
||||
this.audioPlayer.play();
|
||||
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
case "pause":
|
||||
|
||||
case 'pause':
|
||||
if ( this.isPreparedToPlay ) {
|
||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||
this.musicKit.pause();
|
||||
|
||||
return false;
|
||||
} else {
|
||||
this.audioPlayer.pause();
|
||||
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
case "back-10":
|
||||
|
||||
case 'back-10':
|
||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||
this.musicKit.seekToTime( this.musicKit.currentPlaybackTime > 10 ? this.musicKit.currentPlaybackTime - 10 : 0 );
|
||||
|
||||
return false;
|
||||
} else {
|
||||
this.audioPlayer.currentTime = this.audioPlayer.currentTime > 10 ? this.audioPlayer.currentTime - 10 : 0;
|
||||
|
||||
return false;
|
||||
}
|
||||
case "skip-10":
|
||||
|
||||
case 'skip-10':
|
||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||
if ( this.musicKit.currentPlaybackTime < ( this.playlist[ this.playingSongID ].duration - 10 ) ) {
|
||||
this.musicKit.seekToTime( this.musicKit.currentPlaybackTime + 10 );
|
||||
|
||||
return false;
|
||||
} else {
|
||||
if ( this.repeatMode !== 'once' ) {
|
||||
this.control( 'next' );
|
||||
|
||||
return true;
|
||||
} else {
|
||||
this.musicKit.seekToTime( 0 );
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -283,32 +338,42 @@ class MusicKitJSWrapper {
|
||||
this.audioPlayer.currentTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
case "next":
|
||||
|
||||
case 'next':
|
||||
this.control( 'pause' );
|
||||
|
||||
if ( this.queuePos < this.queue.length - 1 ) {
|
||||
this.queuePos += 1;
|
||||
this.prepare( this.queue[ this.queuePos ] );
|
||||
|
||||
return true;
|
||||
} else {
|
||||
this.queuePos = 0;
|
||||
|
||||
if ( this.repeatMode !== 'all' ) {
|
||||
this.control( 'pause' );
|
||||
} else {
|
||||
this.playingSongID = this.queue[ this.queuePos ];
|
||||
this.prepare( this.queue[ this.queuePos ] );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
case "previous":
|
||||
|
||||
case 'previous':
|
||||
this.control( 'pause' );
|
||||
|
||||
if ( this.queuePos > 0 ) {
|
||||
this.queuePos -= 1;
|
||||
this.prepare( this.queue[ this.queuePos ] );
|
||||
|
||||
return true;
|
||||
} else {
|
||||
this.queuePos = this.queue.length - 1;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -317,15 +382,22 @@ class MusicKitJSWrapper {
|
||||
setShuffle ( enabled: boolean ) {
|
||||
this.isShuffleEnabled = enabled;
|
||||
this.queue = [];
|
||||
|
||||
if ( enabled ) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const d = [];
|
||||
|
||||
for ( const el in this.playlist ) {
|
||||
d.push( parseInt( el ) );
|
||||
}
|
||||
this.queue = d.map( value => ( { value, sort: Math.random() } ) )
|
||||
.sort( ( a, b ) => a.sort - b.sort )
|
||||
.map( ( { value } ) => value );
|
||||
|
||||
this.queue = d.map( value => ( {
|
||||
value,
|
||||
'sort': Math.random()
|
||||
} ) )
|
||||
.sort( ( a, b ) => a.sort - b.sort )
|
||||
.map( ( {
|
||||
value
|
||||
} ) => value );
|
||||
this.queue.splice( this.queue.indexOf( this.playingSongID ), 1 );
|
||||
this.queue.push( this.playingSongID );
|
||||
this.queue.reverse();
|
||||
@@ -334,6 +406,7 @@ class MusicKitJSWrapper {
|
||||
this.queue.push( parseInt( song ) );
|
||||
}
|
||||
}
|
||||
|
||||
// Find current song ID in queue
|
||||
for ( const el in this.queue ) {
|
||||
if ( this.queue[ el ] === this.playingSongID ) {
|
||||
@@ -359,29 +432,37 @@ class MusicKitJSWrapper {
|
||||
moveSong ( move: SongMove ) {
|
||||
const newQueue = [];
|
||||
const finishedQueue = [];
|
||||
|
||||
let songID = 0;
|
||||
|
||||
for ( const song in this.playlist ) {
|
||||
if ( this.playlist[ song ].id === move.songID ) {
|
||||
songID = parseInt( song );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for ( const el in this.queue ) {
|
||||
if ( this.queue[ el ] !== songID ) {
|
||||
newQueue.push( this.queue[ el ] );
|
||||
}
|
||||
}
|
||||
|
||||
let hasBeenAdded = false;
|
||||
|
||||
for ( const el in newQueue ) {
|
||||
if ( parseInt( el ) === move.newPos ) {
|
||||
finishedQueue.push( songID );
|
||||
hasBeenAdded = true;
|
||||
}
|
||||
|
||||
finishedQueue.push( newQueue[ el ] );
|
||||
}
|
||||
|
||||
if ( !hasBeenAdded ) {
|
||||
finishedQueue.push( songID );
|
||||
}
|
||||
|
||||
this.queue = finishedQueue;
|
||||
}
|
||||
|
||||
@@ -435,9 +516,11 @@ class MusicKitJSWrapper {
|
||||
*/
|
||||
getQueue (): Song[] {
|
||||
const data = [];
|
||||
|
||||
for ( const el in this.queue ) {
|
||||
data.push( this.playlist[ this.queue[ el ] ] );
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -449,13 +532,14 @@ class MusicKitJSWrapper {
|
||||
getUserPlaylists ( cb: ( data: object ) => void ): boolean {
|
||||
if ( this.isLoggedIn ) {
|
||||
this.apiGetRequest( 'https://api.music.apple.com/v1/me/library/playlists', cb );
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
getPlaying ( ): boolean {
|
||||
getPlaying ( ): boolean {
|
||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||
return this.musicKit.isPlaying;
|
||||
} else {
|
||||
@@ -466,18 +550,21 @@ class MusicKitJSWrapper {
|
||||
findSongOnAppleMusic ( searchTerm: string ): Promise<SearchResult> {
|
||||
// TODO: Make storefront adjustable
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
const queryParameters = {
|
||||
term: ( searchTerm ),
|
||||
types: [ 'songs' ],
|
||||
const queryParameters = {
|
||||
'term': searchTerm,
|
||||
'types': [ 'songs' ],
|
||||
};
|
||||
this.musicKit.api.music( `v1/catalog/ch/search`, queryParameters ).then( results => {
|
||||
|
||||
this.musicKit.api.music( 'v1/catalog/ch/search', queryParameters ).then( results => {
|
||||
resolve( results );
|
||||
} ).catch( e => {
|
||||
console.error( e );
|
||||
reject( e );
|
||||
} );
|
||||
} )
|
||||
.catch( e => {
|
||||
console.error( e );
|
||||
reject( e );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default MusicKitJSWrapper;
|
||||
export default MusicKitJSWrapper;
|
||||
|
||||
@@ -1,34 +1,41 @@
|
||||
/*
|
||||
* 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"
|
||||
import type { SSEMap } from "./song";
|
||||
import {
|
||||
io, type Socket
|
||||
} from 'socket.io-client';
|
||||
import type {
|
||||
SSEMap
|
||||
} from './song';
|
||||
|
||||
class NotificationHandler {
|
||||
|
||||
socket: Socket;
|
||||
|
||||
roomName: string;
|
||||
|
||||
roomToken: string;
|
||||
|
||||
isConnected: boolean;
|
||||
|
||||
useSocket: boolean;
|
||||
|
||||
eventSource?: EventSource;
|
||||
|
||||
toBeListenedForItems: SSEMap;
|
||||
|
||||
reconnectRetryCount: number;
|
||||
|
||||
lastEmitTimestamp: number;
|
||||
|
||||
openConnectionsCount: number;
|
||||
|
||||
pendingRequestCount: number;
|
||||
|
||||
connectionWasSuccessful: boolean;
|
||||
|
||||
constructor () {
|
||||
this.socket = io( localStorage.getItem( 'url' ) ?? '', {
|
||||
autoConnect: false,
|
||||
'autoConnect': false,
|
||||
} );
|
||||
this.roomName = '';
|
||||
this.roomToken = '';
|
||||
@@ -43,35 +50,44 @@ class NotificationHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a room token and connect to
|
||||
* @param {string} roomName
|
||||
* Create a room token and connect to
|
||||
* @param {string} roomName
|
||||
* @param {boolean} useAntiTamper
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
connect ( roomName: string, useAntiTamper: boolean ): Promise<void> {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
fetch( localStorage.getItem( 'url' ) + '/createRoomToken?roomName=' + roomName + '&useAntiTamper=' + useAntiTamper, { credentials: 'include' } ).then( res => {
|
||||
fetch( localStorage.getItem( 'url' ) + '/createRoomToken?roomName=' + roomName + '&useAntiTamper=' + useAntiTamper, {
|
||||
'credentials': 'include'
|
||||
} ).then( res => {
|
||||
if ( res.status === 200 ) {
|
||||
res.text().then( text => {
|
||||
this.roomToken = text;
|
||||
this.roomName = roomName;
|
||||
|
||||
if ( this.useSocket ) {
|
||||
this.socket.connect();
|
||||
this.socket.emit( 'create-room', {
|
||||
name: this.roomName,
|
||||
token: this.roomToken
|
||||
}, ( res: { status: boolean, msg: string } ) => {
|
||||
if ( res.status === true ) {
|
||||
this.isConnected = true;
|
||||
resolve();
|
||||
} else {
|
||||
reject( 'ERR_ROOM_CONNECTING' );
|
||||
this.socket.emit(
|
||||
'create-room', {
|
||||
'name': this.roomName,
|
||||
'token': this.roomToken
|
||||
}, ( res: {
|
||||
'status': boolean,
|
||||
'msg': string
|
||||
} ) => {
|
||||
if ( res.status === true ) {
|
||||
this.isConnected = true;
|
||||
resolve();
|
||||
} else {
|
||||
reject( 'ERR_ROOM_CONNECTING' );
|
||||
}
|
||||
}
|
||||
} );
|
||||
);
|
||||
} else {
|
||||
this.sseConnect().then( () => {
|
||||
resolve();
|
||||
} ).catch( );
|
||||
} )
|
||||
.catch( );
|
||||
}
|
||||
} );
|
||||
} else if ( res.status === 409 ) {
|
||||
@@ -90,9 +106,13 @@ class NotificationHandler {
|
||||
if ( this.reconnectRetryCount < 5 ) {
|
||||
if ( this.openConnectionsCount < 1 && !this.isConnected ) {
|
||||
this.openConnectionsCount += 1;
|
||||
fetch( localStorage.getItem( 'url' ) + '/socket/joinRoom?room=' + this.roomName, { credentials: 'include' } ).then( res => {
|
||||
fetch( localStorage.getItem( 'url' ) + '/socket/joinRoom?room=' + this.roomName, {
|
||||
'credentials': 'include'
|
||||
} ).then( res => {
|
||||
if ( res.status === 200 ) {
|
||||
this.eventSource = new EventSource( localStorage.getItem( 'url' ) + '/socket/connection?room=' + this.roomName, { withCredentials: true } );
|
||||
this.eventSource = new EventSource( localStorage.getItem( 'url' ) + '/socket/connection?room=' + this.roomName, {
|
||||
'withCredentials': true
|
||||
} );
|
||||
|
||||
this.eventSource.onopen = () => {
|
||||
this.isConnected = true;
|
||||
@@ -100,25 +120,26 @@ class NotificationHandler {
|
||||
this.reconnectRetryCount = 0;
|
||||
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Connection successfully established!' );
|
||||
resolve();
|
||||
}
|
||||
|
||||
this.eventSource.onmessage = ( e ) => {
|
||||
};
|
||||
|
||||
this.eventSource.onmessage = e => {
|
||||
const d = JSON.parse( e.data );
|
||||
|
||||
if ( this.toBeListenedForItems[ d.type ] ) {
|
||||
this.toBeListenedForItems[ d.type ]( d.data );
|
||||
}
|
||||
}
|
||||
|
||||
this.eventSource.onerror = ( e ) => {
|
||||
};
|
||||
|
||||
this.eventSource.onerror = e => {
|
||||
if ( this.isConnected ) {
|
||||
this.isConnected = false;
|
||||
this.eventSource?.close();
|
||||
this.openConnectionsCount -= 1;
|
||||
console.debug( e );
|
||||
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Reconnecting due to connection error!' );
|
||||
|
||||
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Reconnecting due to connection error!' );
|
||||
|
||||
this.eventSource = undefined;
|
||||
|
||||
|
||||
this.reconnectRetryCount += 1;
|
||||
setTimeout( () => {
|
||||
this.sseConnect();
|
||||
@@ -131,21 +152,22 @@ class NotificationHandler {
|
||||
} else {
|
||||
reject( 'ERR_ROOM_CONNECTING_STATUS_CODE' );
|
||||
}
|
||||
} ).catch( () => {
|
||||
if ( !this.connectionWasSuccessful ) {
|
||||
reject( 'ERR_ROOM_CONNECTING' );
|
||||
} else {
|
||||
this.openConnectionsCount -= 1;
|
||||
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Reconnecting due to severe connection error!' );
|
||||
|
||||
this.eventSource = undefined;
|
||||
|
||||
this.reconnectRetryCount += 1;
|
||||
setTimeout( () => {
|
||||
this.sseConnect();
|
||||
}, 1000 * this.reconnectRetryCount );
|
||||
}
|
||||
} );
|
||||
} )
|
||||
.catch( () => {
|
||||
if ( !this.connectionWasSuccessful ) {
|
||||
reject( 'ERR_ROOM_CONNECTING' );
|
||||
} else {
|
||||
this.openConnectionsCount -= 1;
|
||||
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Reconnecting due to severe connection error!' );
|
||||
|
||||
this.eventSource = undefined;
|
||||
|
||||
this.reconnectRetryCount += 1;
|
||||
setTimeout( () => {
|
||||
this.sseConnect();
|
||||
}, 1000 * this.reconnectRetryCount );
|
||||
}
|
||||
} );
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
@@ -169,9 +191,14 @@ class NotificationHandler {
|
||||
emit ( event: string, data: any ): void {
|
||||
if ( this.isConnected ) {
|
||||
if ( this.useSocket ) {
|
||||
this.socket.emit( event, { 'roomToken': this.roomToken, 'roomName': this.roomName, 'data': data } );
|
||||
this.socket.emit( event, {
|
||||
'roomToken': this.roomToken,
|
||||
'roomName': this.roomName,
|
||||
'data': data
|
||||
} );
|
||||
} else {
|
||||
const now = new Date().getTime();
|
||||
|
||||
if ( this.lastEmitTimestamp < now - 250 ) {
|
||||
this.lastEmitTimestamp = now;
|
||||
this.sendEmitConventionally( event, data );
|
||||
@@ -189,10 +216,15 @@ class NotificationHandler {
|
||||
|
||||
sendEmitConventionally ( event: string, data: any ): void {
|
||||
fetch( localStorage.getItem( 'url' ) + '/socket/update', {
|
||||
method: 'post',
|
||||
body: JSON.stringify( { 'event': event, 'roomName': this.roomName, 'roomToken': this.roomToken, 'data': data } ),
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'method': 'post',
|
||||
'body': JSON.stringify( {
|
||||
'event': event,
|
||||
'roomName': this.roomName,
|
||||
'roomToken': this.roomToken,
|
||||
'data': data
|
||||
} ),
|
||||
'credentials': 'include',
|
||||
'headers': {
|
||||
'Content-Type': 'application/json',
|
||||
'charset': 'utf-8'
|
||||
}
|
||||
@@ -209,7 +241,7 @@ class NotificationHandler {
|
||||
if ( this.useSocket ) {
|
||||
if ( this.isConnected ) {
|
||||
this.socket.on( event, cb );
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.toBeListenedForItems[ event ] = cb;
|
||||
}
|
||||
@@ -222,22 +254,32 @@ class NotificationHandler {
|
||||
async disconnect (): Promise<void> {
|
||||
if ( this.isConnected ) {
|
||||
if ( this.useSocket ) {
|
||||
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!' );
|
||||
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!' );
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
} );
|
||||
);
|
||||
} else {
|
||||
fetch( localStorage.getItem( 'url' ) + '/socket/deleteRoom', {
|
||||
method: 'post',
|
||||
body: JSON.stringify( { 'roomName': this.roomName, 'roomToken': this.roomToken } ),
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'method': 'post',
|
||||
'body': JSON.stringify( {
|
||||
'roomName': this.roomName,
|
||||
'roomToken': this.roomToken
|
||||
} ),
|
||||
'credentials': 'include',
|
||||
'headers': {
|
||||
'Content-Type': 'application/json',
|
||||
'charset': 'utf-8'
|
||||
}
|
||||
@@ -247,10 +289,12 @@ class NotificationHandler {
|
||||
} else {
|
||||
alert( 'Unable to delete the room you were just in. The name will be blocked until the next server restart!' );
|
||||
}
|
||||
|
||||
return;
|
||||
} ).catch( () => {
|
||||
return;
|
||||
} );
|
||||
} )
|
||||
.catch( () => {
|
||||
return;
|
||||
} );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -258,6 +302,7 @@ class NotificationHandler {
|
||||
getRoomName (): string {
|
||||
return this.roomName;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default NotificationHandler;
|
||||
|
||||
72
MusicPlayerV2-GUI/src/scripts/song.d.ts
vendored
72
MusicPlayerV2-GUI/src/scripts/song.d.ts
vendored
@@ -4,92 +4,92 @@ export interface Song {
|
||||
/**
|
||||
* The ID. Either the apple music ID, or if from local disk, an ID starting in local_
|
||||
*/
|
||||
id: string;
|
||||
'id': string;
|
||||
|
||||
/**
|
||||
* Origin of the song
|
||||
*/
|
||||
origin: Origin;
|
||||
'origin': Origin;
|
||||
|
||||
/**
|
||||
* The cover image as a URL
|
||||
*/
|
||||
cover: string;
|
||||
'cover': string;
|
||||
|
||||
/**
|
||||
* The artist of the song
|
||||
*/
|
||||
artist: string;
|
||||
'artist': string;
|
||||
|
||||
/**
|
||||
* The name of the song
|
||||
*/
|
||||
title: string;
|
||||
'title': string;
|
||||
|
||||
/**
|
||||
* Duration of the song in milliseconds
|
||||
*/
|
||||
duration: number;
|
||||
'duration': number;
|
||||
|
||||
/**
|
||||
* (OPTIONAL) The genres this song belongs to. Can be displayed on the showcase screen, but requires settings there
|
||||
*/
|
||||
genres?: string[];
|
||||
'genres'?: string[];
|
||||
|
||||
/**
|
||||
* (OPTIONAL) This will be displayed in brackets on the showcase screens
|
||||
*/
|
||||
additionalInfo?: string;
|
||||
'additionalInfo'?: string;
|
||||
}
|
||||
|
||||
export interface SongTransmitted {
|
||||
title: string;
|
||||
artist: string;
|
||||
duration: number;
|
||||
cover: string;
|
||||
additionalInfo?: string;
|
||||
'title': string;
|
||||
'artist': string;
|
||||
'duration': number;
|
||||
'cover': string;
|
||||
'additionalInfo'?: string;
|
||||
}
|
||||
|
||||
|
||||
export interface ReadFile {
|
||||
url: string;
|
||||
filename: string;
|
||||
'url': string;
|
||||
'filename': string;
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
data: {
|
||||
results: {
|
||||
songs: {
|
||||
data: AppleMusicSongData[],
|
||||
href: string;
|
||||
'data': {
|
||||
'results': {
|
||||
'songs': {
|
||||
'data': AppleMusicSongData[],
|
||||
'href': string;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface AppleMusicSongData {
|
||||
id: string,
|
||||
type: string;
|
||||
href: string;
|
||||
attributes: {
|
||||
albumName: string;
|
||||
artistName: string;
|
||||
artwork: {
|
||||
width: number,
|
||||
height: number,
|
||||
url: string
|
||||
'id': string,
|
||||
'type': string;
|
||||
'href': string;
|
||||
'attributes': {
|
||||
'albumName': string;
|
||||
'artistName': string;
|
||||
'artwork': {
|
||||
'width': number,
|
||||
'height': number,
|
||||
'url': string
|
||||
},
|
||||
name: string;
|
||||
genreNames: string[];
|
||||
durationInMillis: number;
|
||||
'name': string;
|
||||
'genreNames': string[];
|
||||
'durationInMillis': number;
|
||||
}
|
||||
}
|
||||
|
||||
export interface SongMove {
|
||||
songID: string;
|
||||
newPos: number;
|
||||
'songID': string;
|
||||
'newPos': number;
|
||||
}
|
||||
|
||||
export interface SSEMap {
|
||||
[key: string]: ( data: any ) => void;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user