/* * MusicPlayerV2 - index.js * * Created by Janis Hutz 11/20/2023, Licensed under the GPL V3 License * https://janishutz.com, development@janishutz.com * * */ /* Quick side note here: This is terribly ugly code, but I was in a hurry to finish it, so I had no time to clean it up. I will do that at some point -jh */ const app = Vue.createApp( { data() { return { musicKit: null, isLoggedIn: false, config: { 'devToken': '', 'userToken': '' }, playlists: {}, hasSelectedPlaylist: false, songQueue: {}, queuePos: 0, pos: 0, playingSong: {}, isPlaying: false, isShuffleEnabled: false, repeatMode: 'off', playbackPosBeautified: '', durationBeautified: '', isShowingRemainingTime: false, localIP: '', hasLoadedPlaylists: false, isPreparingToPlay: false, additionalSongInfo: {}, hasFinishedInit: false, isShowingWarning: false, // For use with playlists that are partially from apple music and // local drive isUsingCustomPlaylist: false, rawLoadedPlaylistData: {}, basePath: '', audioPlayer: null, isReconnecting: false, // slider offset: 0, isDragging: false, sliderPos: 0, originalPos: 0, sliderProgress: 0, active: false, } }, methods: { logInto() { if ( !this.musicKit.isAuthorized ) { this.musicKit.authorize().then( () => { this.isLoggedIn = true; this.initMusicKit(); } ); } else { this.musicKit.authorize().then( () => { this.isLoggedIn = true; this.initMusicKit(); } ); } }, initMusicKit () { fetch( '/getAppleMusicDevToken' ).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.playlistHandler ); } ); } ); } } ); }, playlistHandler ( data ) { if ( data.status === 'ok' ) { const d = data.data.data; this.playlist = {}; for ( let el in d ) { this.playlists[ d[ el ].id ] = { title: d[ el ].attributes.name, id: d[ el ].id, playParams: d[ el ].attributes.playParams, } } this.hasLoadedPlaylists = true; } }, apiGetRequest( url, callback ) { 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 ) {} } ); } else { try { callback( { 'status': 'error', 'error': res.status } ); } catch( err ) {} } } ); } else return false; }, getAdditionalSongInfo() { if ( Object.keys( this.additionalSongInfo ).length < 1 ) { fetch( '/apple-music/getAdditionalData' ).then( res => { if ( res.status === 200 ) { res.json().then( json => { this.additionalSongInfo = json; this.handleAdditionalData(); } ); } } ); } }, handleAdditionalData () { if ( Object.keys( this.additionalSongInfo ).length > 0 ) { for ( let item in this.songQueue ) { if ( this.additionalSongInfo[ item ] ) { for ( let d in this.additionalSongInfo[ item ] ) { if ( !this.songQueue[ item ][ d ] ) { this.songQueue[ item ][ d ] = this.additionalSongInfo[ item ][ d ]; } } } } this.playingSong = this.songQueue[ this.queuePos ]; this.sendUpdate( 'songQueue' ); this.sendUpdate( 'playingSong' ); } }, selectPlaylist( id ) { this.isPreparingToPlay = true; this.musicKit.setQueue( { playlist: id } ).then( () => { try { this.loadPlaylist(); this.hasSelectedPlaylist = true; this.isPreparingToPlay = false; } catch( err ) { this.hasSelectedPlaylist = false; console.error( err ); alert( 'We were unable to play. Please ensure that DRM (yeah sorry it is Apple Music, we cannot do anything about that) is enabled and working' ); } } ).catch( err => { console.error( 'ERROR whilst settings Queue', err ); } ); }, handleDrag( e ) { if ( this.isDragging ) { if ( 0 < this.originalPos + e.screenX - this.offset && this.originalPos + e.screenX - this.offset < document.getElementById( 'progress-slider' ).clientWidth - 5 ) { this.sliderPos = e.screenX - this.offset; this.calcProgressPos(); } } }, startMove( e ) { this.offset = e.screenX; this.isDragging = true; document.getElementById( 'drag-support' ).classList.add( 'drag-support-active' ); }, stopMove() { this.originalPos += parseInt( this.sliderPos ); this.isDragging = false; this.offset = 0; this.sliderPos = 0; document.getElementById( 'drag-support' ).classList.remove( 'drag-support-active' ); this.calcPlaybackPos(); }, setPos ( e ) { if ( this.hasSelectedPlaylist ) { this.originalPos = e.offsetX; this.calcProgressPos(); this.calcPlaybackPos(); if ( this.playingSong.origin === 'apple-music' ) { this.musicKit.seekToTime( this.pos ); } else { this.audioPlayer.currentTime = this.pos; } this.sendUpdate( 'pos' ); } }, calcProgressPos() { this.sliderProgress = Math.ceil( ( this.originalPos + parseInt( this.sliderPos ) ) / ( document.getElementById( 'progress-slider' ).clientWidth - 5 ) * 1000 ); }, calcPlaybackPos() { this.pos = Math.round( ( this.originalPos + parseInt( this.sliderPos ) ) / ( document.getElementById( 'progress-slider' ).clientWidth - 5 ) * this.playingSong.duration ); }, sendUpdate( update ) { let data = {}; let up = update; if ( update === 'pos' ) { data = this.pos; } else if ( update === 'playingSong' ) { data = this.playingSong; } else if ( update === 'isPlaying' ) { data = this.isPlaying; } else if ( update === 'songQueue' ) { data = this.songQueue; } else if ( update === 'queuePos' ) { data = this.queuePos; } else if ( update === 'posReset' ) { data = 0; up = 'pos'; } let fetchOptions = { method: 'post', body: JSON.stringify( { 'type': up, 'data': data } ), headers: { 'Content-Type': 'application/json', 'charset': 'utf-8' }, }; fetch( 'http://localhost:8081/statusUpdate', fetchOptions ).catch( err => { console.error( err ); } ); }, loadPlaylist() { const songQueue = this.musicKit.queue.items; for ( let item in songQueue ) { this.songQueue[ item ] = { 'artist': songQueue[ item ].attributes.artistName, 'title': songQueue[ item ].attributes.name, 'year': songQueue[ item ].attributes.releaseDate, 'genre': songQueue[ item ].attributes.genreNames, 'duration': Math.round( songQueue[ item ].attributes.durationInMillis / 1000 ), 'filename': songQueue[ item ].id, 'coverArtOrigin': 'api', 'hasCoverArt': true, 'queuePos': item, 'origin': 'apple-music', } let url = songQueue[ item ].attributes.artwork.url; url = url.replace( '{w}', songQueue[ item ].attributes.artwork.width ); url = url.replace( '{h}', songQueue[ item ].attributes.artwork.height ); this.songQueue[ item ][ 'coverArtURL' ] = url; this.handleAdditionalData(); this.sendUpdate( 'songQueue' ); } }, control( action ) { if ( action === 'play' ) { if ( !this.playingSong.origin ) { this.play( this.songQueue[ 0 ] ); this.isPlaying = true; } else { if ( this.playingSong.origin === 'apple-music' ) { if( !this.musicKit || !this.isPlaying ) { this.musicKit.play().then( () => { this.sendUpdate( 'pos' ); } ).catch( err => { console.log( 'player failed to start' ); console.log( err ); } ); } else { this.musicKit.pause().then( () => { this.musicKit.play().catch( err => { console.log( 'player failed to start' ); console.log( err ); } ); } ); } try { this.audioPlayer.pause(); } catch ( err ) {} } else { this.audioPlayer.play(); this.musicKit.pause(); } this.isPlaying = true; try { clearInterval( this.progressTracker ); } catch( err ) {}; this.progressTracker = setInterval( () => { if ( this.playingSong.origin === 'apple-music' ) { this.pos = parseInt( this.musicKit.currentPlaybackTime ); } else { this.pos = parseInt( this.audioPlayer.currentTime ); } if ( this.pos > this.playingSong.duration - 1 ) { this.control( 'next' ); } const minuteCount = Math.floor( this.pos / 60 ); this.playbackPosBeautified = minuteCount + ':'; if ( ( '' + minuteCount ).length === 1 ) { this.playbackPosBeautified = '0' + minuteCount + ':'; } const secondCount = Math.floor( this.pos - minuteCount * 60 ); if ( ( '' + secondCount ).length === 1 ) { this.playbackPosBeautified += '0' + secondCount; } else { this.playbackPosBeautified += secondCount; } if ( this.isShowingRemainingTime ) { const minuteCounts = Math.floor( ( this.playingSong.duration - this.pos ) / 60 ); this.durationBeautified = '-' + String( minuteCounts ) + ':'; if ( ( '' + minuteCounts ).length === 1 ) { this.durationBeautified = '-0' + minuteCounts + ':'; } const secondCounts = Math.floor( ( this.playingSong.duration - this.pos ) - minuteCounts * 60 ); if ( ( '' + secondCounts ).length === 1 ) { this.durationBeautified += '0' + secondCounts; } else { this.durationBeautified += secondCounts; } } }, 50 ); this.sendUpdate( 'pos' ); this.sendUpdate( 'isPlaying' ); } } else if ( action === 'pause' ) { if ( this.playingSong.origin === 'apple-music' ) { this.musicKit.pause(); } else { this.audioPlayer.pause(); } this.sendUpdate( 'pos' ); try { clearInterval( this.progressTracker ); clearInterval( this.notifier ); } catch ( err ) {}; this.isPlaying = false; this.sendUpdate( 'isPlaying' ); } else if ( action === 'replay10' ) { if ( this.playingSong.origin === 'apple-music' ) { this.musicKit.seekToTime( this.musicKit.currentPlaybackTime > 10 ? this.musicKit.currentPlaybackTime - 10 : 0 ); this.pos = this.musicKit.currentPlaybackTime; } else { this.audioPlayer.currentTime = this.audioPlayer.currentTime > 10 ? this.audioPlayer.currentTime - 10 : 0; this.pos = this.audioPlayer.currentTime; } this.sendUpdate( 'pos' ); } else if ( action === 'forward10' ) { if ( this.playingSong.origin === 'apple-music' ) { if ( this.musicKit.currentPlaybackTime < ( this.playingSong.duration - 10 ) ) { this.musicKit.seekToTime( this.musicKit.currentPlaybackTime + 10 ); this.pos = this.musicKit.currentPlaybackTime; this.sendUpdate( 'pos' ); } else { if ( this.repeatMode !== 'one' ) { this.control( 'next' ); } else { this.musicKit.seekToTime( 0 ); this.pos = this.musicKit.currentPlaybackTime; this.sendUpdate( 'pos' ); } } } else { if ( this.audioPlayer.currentTime < ( this.playingSong.duration - 10 ) ) { this.audioPlayer.currentTime = this.audioPlayer.currentTime + 10; this.pos = this.audioPlayer.currentTime; this.sendUpdate( 'pos' ); } else { if ( this.repeatMode !== 'one' ) { this.control( 'next' ); } else { this.audioPlayer.currentTime = 0; this.pos = this.audioPlayer.currentTime; this.sendUpdate( 'pos' ); } } } } else if ( action === 'reset' ) { clearInterval( this.progressTracker ); this.pos = 0; if ( this.playingSong.origin === 'apple-music' ) { this.musicKit.seekToTime( 0 ); } else { this.audioPlayer.currentTime = 0; } this.sendUpdate( 'pos' ); } else if ( action === 'next' ) { if ( this.queuePos < parseInt( Object.keys( this.songQueue ).length ) - 1 ) { this.queuePos = parseInt( this.queuePos ) + 1; this.play( this.songQueue[ this.queuePos ] ); } else { if ( this.repeatMode === 'all' ) { this.queuePos = 0; this.play( this.songQueue[ 0 ] ); } else { this.control( 'pause' ); } } } else if ( action === 'previous' ) { if ( this.pos > 3 ) { this.pos = 0; if ( this.isUsingCustomPlaylist ) { this.audioPlayer.currentTime = 0; this.sendUpdate( 'pos' ); } else { this.musicKit.seekToTime( 0 ).then( () => { this.sendUpdate( 'pos' ); this.control( 'play' ); } ); } } else { if ( this.queuePos > 0 ) { this.queuePos = parseInt( this.queuePos ) - 1; this.play( this.songQueue[ this.queuePos ] ); } else { this.queuePos = parseInt( Object.keys( this.songQueue ).length ) - 1; this.play[ this.songQueue[ this.queuePos ] ]; } } } else if ( action === 'shuffleOff' ) { // TODO: Make shuffle function this.isShuffleEnabled = false; // this.loadPlaylist(); alert( 'not implemented yet' ); } else if ( action === 'shuffleOn' ) { this.isShuffleEnabled = true; alert( 'not implemented yet' ); // this.loadPlaylist(); } else if ( action === 'repeatOne' ) { this.repeatMode = 'one'; } else if ( action === 'repeatAll' ) { this.repeatMode = 'all'; } else if ( action === 'repeatOff' ) { this.musicKit.repeatMode = MusicKit.PlayerRepeatMode.none; this.repeatMode = 'off'; } }, play( song, specificID ) { let foundSong = specificID ?? 0; if ( !specificID ) { for ( let s in this.songQueue ) { if ( this.songQueue[ s ] === song ) { foundSong = s; } } } this.queuePos = foundSong; this.sendUpdate( 'queuePos' ); this.pos = 0; this.sendUpdate( 'posReset' ); this.playingSong = song; this.sendUpdate( 'playingSong' ); if ( song.origin === 'apple-music' ) { this.musicKit.setQueue( { 'song': song.filename } ).then( () => { setTimeout( () => { this.control( 'play' ); }, 500 ); } ).catch( ( err ) => { console.log( err ); } ); } else { setTimeout( () => { this.control( 'play' ); }, 500 ); } const minuteCounts = Math.floor( ( this.playingSong.duration ) / 60 ); this.durationBeautified = String( minuteCounts ) + ':'; if ( ( '' + minuteCounts ).length === 1 ) { this.durationBeautified = '0' + minuteCounts + ':'; } const secondCounts = Math.floor( ( this.playingSong.duration ) - minuteCounts * 60 ); if ( ( '' + secondCounts ).length === 1 ) { this.durationBeautified += '0' + secondCounts; } else { this.durationBeautified += secondCounts; } this.hasFinishedInit = true; }, toggleShowMode() { this.isShowingRemainingTime = !this.isShowingRemainingTime; }, exportCurrentPlaylist() { let fetchOptions = { method: 'post', body: JSON.stringify( this.songQueue ), headers: { 'Content-Type': 'application/json', 'charset': 'utf-8' }, }; fetch( '/savePlaylist', fetchOptions ).then( res => { if ( res.status === 200 ) { console.log( 'saved' ); } } ); }, selectPlaylistFromDisk() { this.isPreparingToPlay = true; let playlistSongs = []; fetch( '/loadPlaylist' ).then( res => { res.json().then( data => { this.rawLoadedPlaylistData = data.data; this.basePath = data.path.slice( 0, data.path.lastIndexOf( '/' ) ); for ( let song in this.rawLoadedPlaylistData ) { if ( this.rawLoadedPlaylistData[ song ].origin === 'apple-music' ) { playlistSongs.push( this.rawLoadedPlaylistData[ song ].filename ); } } this.musicKit.setQueue( { songs: playlistSongs } ).then( () => { this.isUsingCustomPlaylist = true; try { this.loadCustomPlaylist(); this.hasSelectedPlaylist = true; this.isPreparingToPlay = false; } catch( err ) { this.hasSelectedPlaylist = false; console.error( err ); alert( 'We were unable to play. Please ensure that DRM (yeah sorry it is Apple Music, we cannot do anything about that) is enabled and working' ); } } ).catch( err => { console.error( 'ERROR whilst settings Queue', err ); } ); } ); } ); }, loadCustomPlaylist() { const songQueue = this.musicKit.queue.items; let offset = 0; ( async() => { for ( let item in this.rawLoadedPlaylistData ) { if ( this.rawLoadedPlaylistData[ item ].origin === 'apple-music' ) { this.songQueue[ item ] = { 'artist': songQueue[ item - offset ].attributes.artistName, 'title': songQueue[ item - offset ].attributes.name, 'year': songQueue[ item - offset ].attributes.releaseDate, 'genre': songQueue[ item - offset ].attributes.genreNames, 'duration': Math.round( songQueue[ item - offset ].attributes.durationInMillis / 1000 ), 'filename': songQueue[ item - offset ].id, 'coverArtOrigin': 'api', 'hasCoverArt': true, 'queuePos': item, 'origin': 'apple-music', 'offset': offset, } let url = songQueue[ item - offset ].attributes.artwork.url; url = url.replace( '{w}', songQueue[ item - offset ].attributes.artwork.width ); url = url.replace( '{h}', songQueue[ item - offset ].attributes.artwork.height ); this.songQueue[ item ][ 'coverArtURL' ] = url; } else { offset += 1; const queryParameters = { term: ( this.rawLoadedPlaylistData[ item ].artist ?? '' ) + ' ' + ( this.rawLoadedPlaylistData[ item ].title ?? '' ), types: [ 'songs' ], }; // TODO: Make storefront adjustable const result = await this.musicKit.api.music( '/v1/catalog/ch/search', queryParameters ); let json; try { const res = await fetch( '/getMetadata?file=' + this.basePath + '/' + this.rawLoadedPlaylistData[ item ].filename ); json = await res.json(); } catch( err ) {} if ( result.data ) { if ( result.data.results.songs ) { const dat = result.data.results.songs.data[ 0 ]; console.log( json ); this.songQueue[ item ] = { 'artist': dat.attributes.artistName, 'title': dat.attributes.name, 'year': dat.attributes.releaseDate, 'genre': dat.attributes.genreNames, 'duration': json ? Math.round( json.duration ) : undefined, 'filename': this.rawLoadedPlaylistData[ item ].filename, 'coverArtOrigin': 'api', 'hasCoverArt': true, 'queuePos': item, 'origin': 'local', } let url = dat.attributes.artwork.url; url = url.replace( '{w}', dat.attributes.artwork.width ); url = url.replace( '{h}', dat.attributes.artwork.height ); this.songQueue[ item ][ 'coverArtURL' ] = url; } } } this.handleAdditionalData(); this.sendUpdate( 'songQueue' ); } } )(); setTimeout( () => { this.audioPlayer = document.getElementById( 'audio-player' ); }, 1000 ); }, search() { ( async() => { const searchTerm = prompt( 'Enter search term...' ) const queryParameters = { term: ( searchTerm ), types: [ 'songs' ], }; // TODO: Make storefront adjustable const result = await this.musicKit.api.music( '/v1/catalog/ch/search', queryParameters ); console.log( result ); } )(); }, connectToNotifier() { let source = new EventSource( '/mainNotifier', { withCredentials: true } ); source.onmessage = ( e ) => { let data; try { data = JSON.parse( e.data ); } catch ( err ) { data = { 'type': e.data }; } if ( data.type === 'blur' ) { this.isShowingWarning = true; } else if ( data.type === 'visibility' ) { this.isShowingWarning = true; } }; source.onopen = () => { this.isReconnecting = false; console.log( 'client notifier connected successfully' ); }; let self = this; source.addEventListener( 'error', function( e ) { if ( e.eventPhase == EventSource.CLOSED ) source.close(); if ( e.target.readyState == EventSource.CLOSED ) { setTimeout( () => { if ( !self.isReconnecting ) { console.log( 'disconnected' ); console.log( 'reconnecting...' ); self.isReconnecting = true; self.tryReconnect(); } }, 1000 ); } }, false ); }, tryReconnect() { const int = setInterval( () => { if ( !this.isReconnecting ) { clearInterval( int ); } else { this.connectToNotifier(); } }, 1000 ); }, dismissNotification() { this.isShowingWarning = false; } }, watch: { pos() { if ( !this.isDragging ) { this.sliderProgress = Math.ceil( this.pos / this.playingSong.duration * 1000 + 2 ); this.originalPos = Math.ceil( this.pos / this.playingSong.duration * ( document.getElementById( 'progress-slider' ).scrollWidth - 5 ) ); } } }, created() { document.addEventListener( 'keydown', ( e ) => { if ( e.key === ' ' ) { e.preventDefault(); if ( !this.isPlaying ) { this.control( 'play' ); } else { this.control( 'pause' ); } } else if ( e.key === 'ArrowRight' ) { e.preventDefault(); this.control( 'next' ); } else if ( e.key === 'ArrowLeft' ) { e.preventDefault(); this.control( 'previous' ); } } ); if ( !window.MusicKit ) { document.addEventListener( 'musickitloaded', () => { self.initMusicKit(); } ); } else { this.initMusicKit(); } this.connectToNotifier(); fetch( '/getLocalIP' ).then( res => { if ( res.status === 200 ) { res.text().then( ip => { this.localIP = ip; } ); } } ); }, } ).mount( '#app' );