mirror of
https://github.com/janishutz/MusicPlayerV2.git
synced 2025-11-26 05:14:24 +00:00
basically done (at least the essential part)
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
import ColorThief from 'colorthief';
|
||||
const colorThief = new ColorThief();
|
||||
|
||||
const getImageData = (): Promise<number[][]> => {
|
||||
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,
|
||||
}
|
||||
81
MusicPlayerV2-GUI/src/scripts/connection.ts
Normal file
81
MusicPlayerV2-GUI/src/scripts/connection.ts
Normal file
@@ -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<string>}
|
||||
*/
|
||||
connect (): Promise<any> {
|
||||
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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user