mirror of
https://github.com/janishutz/MusicPlayerV2.git
synced 2025-11-25 13:04:23 +00:00
some progress on player + playlist loading
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200">
|
||||
<!-- TODO: Update URL -->
|
||||
<script src="/musickit.js"></script>
|
||||
<script src="https://js-cdn.music.apple.com/musickit/v3/musickit.js"></script>
|
||||
<title>MusicPlayer</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,10 +1,18 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Library</h1>
|
||||
<playlistsView></playlistsView>
|
||||
<playlistsView :playlists="$props.playlists"></playlistsView>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import playlistsView from '@/components/playlistsView.vue';
|
||||
|
||||
defineProps( {
|
||||
'playlists': {
|
||||
'default': [],
|
||||
'type': Array<any>,
|
||||
'required': true,
|
||||
}
|
||||
} );
|
||||
</script>
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
import { ref } from 'vue';
|
||||
import playlistView from '@/components/playlistView.vue';
|
||||
import MusicKitJSWrapper from '@/scripts/player';
|
||||
import MusicKitJSWrapper from '@/scripts/music-player';
|
||||
|
||||
const isPlaying = ref( false );
|
||||
const repeatMode = ref( '' );
|
||||
@@ -46,9 +46,9 @@
|
||||
isPlaying.value = !isPlaying.value;
|
||||
// TODO: Execute function on player
|
||||
if ( isPlaying.value ) {
|
||||
player.play();
|
||||
player.control( 'play' );
|
||||
} else {
|
||||
player.pause();
|
||||
player.control( 'pause' );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,8 +85,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
const getPlaylists = ( cb: ( data: object ) => void ) => {
|
||||
player.getUserPlaylists( cb );
|
||||
}
|
||||
|
||||
const logIntoAppleMusic = () => {
|
||||
player.logIn();
|
||||
}
|
||||
|
||||
const getAuth = (): boolean[] => {
|
||||
return player.getAuth();
|
||||
}
|
||||
|
||||
const skipLogin = () => {
|
||||
player.init();
|
||||
}
|
||||
|
||||
defineExpose( {
|
||||
logIntoAppleMusic,
|
||||
getPlaylists,
|
||||
controlUI,
|
||||
getAuth,
|
||||
skipLogin,
|
||||
} );
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
<template>
|
||||
<div>
|
||||
<h3>Your playlists</h3>
|
||||
<div v-for="pl in $props.playlists" v-bind:key="pl.id">
|
||||
{{ pl.attributes.name }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps( {
|
||||
'playlists': {
|
||||
'default': [],
|
||||
'type': Array<any>,
|
||||
'required': true,
|
||||
}
|
||||
} )
|
||||
</script>
|
||||
361
MusicPlayerV2-GUI/src/scripts/music-player.ts
Normal file
361
MusicPlayerV2-GUI/src/scripts/music-player.ts
Normal file
@@ -0,0 +1,361 @@
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
constructor () {
|
||||
this.playingSongID = 0;
|
||||
this.playlist = [];
|
||||
this.queue = [];
|
||||
this.config = {
|
||||
devToken: '',
|
||||
userToken: '',
|
||||
};
|
||||
this.isShuffleEnabled = false;
|
||||
this.repeatMode = 'off';
|
||||
this.isPreparedToPlay = false;
|
||||
this.isLoggedIn = false;
|
||||
this.hasEncounteredAuthError = false;
|
||||
|
||||
const self = this;
|
||||
|
||||
if ( !window.MusicKit ) {
|
||||
document.addEventListener( 'musickitloaded', () => {
|
||||
self.init();
|
||||
} );
|
||||
} else {
|
||||
this.init();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a user into Apple Music. Will automatically initialize MusicKitJS, once user is logged in
|
||||
* @returns {void}
|
||||
*/
|
||||
logIn (): void {
|
||||
if ( !this.musicKit.isAuthorized ) {
|
||||
this.musicKit.authorize().then( () => {
|
||||
this.isLoggedIn = true;
|
||||
this.init();
|
||||
} ).catch( () => {
|
||||
this.hasEncounteredAuthError = true;
|
||||
} );
|
||||
} else {
|
||||
this.musicKit.authorize().then( () => {
|
||||
this.isLoggedIn = true;
|
||||
this.init();
|
||||
} ).catch( () => {
|
||||
this.hasEncounteredAuthError = true;
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize MusicKitJS. Should not be called. Use logIn instead, which first tries to log the user in, then calls this method.
|
||||
* @returns {void}
|
||||
*/
|
||||
init (): void {
|
||||
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: '3'
|
||||
},
|
||||
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;
|
||||
} );
|
||||
} );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the authentication status of the user
|
||||
* @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 ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Request data from the Apple Music API
|
||||
* @param {string} url The URL at the Apple Music API to call (including protocol and url)
|
||||
* @param {( data: object ) => void} callback A callback function that takes the data and returns nothing
|
||||
* @returns {void}
|
||||
*/
|
||||
apiGetRequest ( url: string, callback: ( data: object ) => void ): 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the playlist to play
|
||||
* @param {Song[]} playlist The playlist as an array of songs
|
||||
* @returns {void}
|
||||
*/
|
||||
setPlaylist ( playlist: Song[] ): void {
|
||||
this.playlist = playlist;
|
||||
this.setShuffle( this.isShuffleEnabled );
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a specific song in the queue for playing and start playing
|
||||
* @param {number} playlistID The ID of the song in the playlist to prepare to play
|
||||
* @returns {boolean} Returns true, if successful, false, if playlist is missing / empty. Set that first
|
||||
*/
|
||||
prepare ( playlistID: number ): boolean {
|
||||
if ( this.playlist.length > 0 ) {
|
||||
this.playingSongID = playlistID;
|
||||
this.isPreparedToPlay = true;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Control the player
|
||||
* @param {ControlAction} action Action to take on the player
|
||||
* @returns {void}
|
||||
*/
|
||||
control ( action: ControlAction ): void {
|
||||
switch ( action ) {
|
||||
case "play":
|
||||
if ( this.isPreparedToPlay ) {
|
||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||
this.musicKit.play();
|
||||
} else {
|
||||
// TODO: Implement
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "pause":
|
||||
if ( this.isPreparedToPlay ) {
|
||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||
this.musicKit.pause();
|
||||
} else {
|
||||
// TODO: Implement
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "back-10":
|
||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||
this.musicKit.seekToTime( this.musicKit.currentPlaybackTime > 10 ? this.musicKit.currentPlaybackTime - 10 : 0 );
|
||||
} else {
|
||||
// TODO: Implement
|
||||
}
|
||||
break;
|
||||
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 );
|
||||
} else {
|
||||
if ( this.repeatMode !== 'once' ) {
|
||||
this.control( 'next' );
|
||||
} else {
|
||||
this.musicKit.seekToTime( 0 );
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// TODO: Finish
|
||||
// if ( this.audioPlayer.currentTime < ( this.playlist[ this.playingSongID ].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' );
|
||||
// }
|
||||
// }
|
||||
}
|
||||
break;
|
||||
case "next":
|
||||
//
|
||||
break;
|
||||
case "previous":
|
||||
}
|
||||
}
|
||||
|
||||
setShuffle ( enabled: boolean ) {
|
||||
this.isShuffleEnabled = enabled;
|
||||
// TODO: Shuffle playlist
|
||||
}
|
||||
|
||||
setRepeatMode ( mode: RepeatMode ) {
|
||||
this.repeatMode = mode;
|
||||
}
|
||||
|
||||
goToPos ( pos: number ) {
|
||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||
this.musicKit.seekToTime( pos );
|
||||
} else {
|
||||
// TODO: Implement
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current position of the play heed. Will return in ms since start of the song
|
||||
* @returns {number}
|
||||
*/
|
||||
getPlaybackPos (): number {
|
||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||
return this.musicKit.currentPlaybackTime;
|
||||
} else {
|
||||
return 0;
|
||||
// TODO: Implement
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get details on the currently playing song
|
||||
* @returns {Song}
|
||||
*/
|
||||
getPlayingSong (): Song {
|
||||
return this.playlist[ this.playingSongID ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the playlist index of the currently playing song
|
||||
* @returns {number}
|
||||
*/
|
||||
getPlayingSongID (): number {
|
||||
return this.playingSongID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full playlist, as it is set currently, not ordered by queue settings, but as passed in originally
|
||||
* @returns {Song[]}
|
||||
*/
|
||||
getPlaylist (): Song[] {
|
||||
return this.playlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as getPlaylist, but returns a ordered playlist, by how it will play according to the queue.
|
||||
* @returns {Song[]}
|
||||
*/
|
||||
getQueue (): Song[] {
|
||||
const data = [];
|
||||
for ( const el in this.queue ) {
|
||||
data.push( this.playlist[ this.queue[ el ] ] );
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all playlists the authenticated user has on Apple Music. Only available once the user has authenticated!
|
||||
* @param {( data: object ) => void} cb The callback function called with the results from the API
|
||||
* @returns {boolean} Returns true, if user is authenticated and request was started, false if not.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// findSongOnAppleMusic ( searchTerm: string ): Song => {
|
||||
// TODO: Implement
|
||||
// }
|
||||
}
|
||||
|
||||
export default MusicKitJSWrapper;
|
||||
@@ -1,3 +1,6 @@
|
||||
// IMPORTANT: Old, unfinished version that doesn't ship! See ./music-player.ts for the actual code!
|
||||
|
||||
|
||||
type Origin = 'apple-music' | 'disk';
|
||||
|
||||
interface Song {
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
<template>
|
||||
<div class="app-view">
|
||||
<div class="home-view" v-if="isLoggedIntoAppleMusic">
|
||||
<libraryView class="library-view"></libraryView>
|
||||
<playerView :class="'player-view' + ( isShowingFullScreenPlayer ? ' full-screen-player' : '' )" @player-state-change="( state ) => { handlePlayerStateChange( state ) }"></playerView>
|
||||
<libraryView class="library-view" :playlists="playlists"></libraryView>
|
||||
</div>
|
||||
<div v-else class="login-view">
|
||||
<img src="@/assets/appleMusicIcon.svg" alt="Apple Music Icon">
|
||||
<button class="fancy-button" style="margin-top: 20px;">Log into Apple Music</button>
|
||||
<button class="fancy-button" style="margin-top: 20px;" @click="logIntoAppleMusic()">Log into Apple Music</button>
|
||||
<button class="fancy-button" title="This allows you to use local playlists only. Cover images for your songs will be fetched from the apple music api as good as possible" @click="skipLogin()">Continue without logging in</button>
|
||||
</div>
|
||||
<playerView :class="'player-view' + ( isLoggedIntoAppleMusic ? ( isShowingFullScreenPlayer ? ' full-screen-player' : '' ) : ' player-hidden' )" @player-state-change="( state ) => { handlePlayerStateChange( state ) }"
|
||||
ref="player"></playerView>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -16,8 +18,10 @@
|
||||
import libraryView from '@/components/libraryView.vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const isLoggedIntoAppleMusic = ref( true );
|
||||
const isLoggedIntoAppleMusic = ref( false );
|
||||
const isShowingFullScreenPlayer = ref( false );
|
||||
const player = ref( playerView );
|
||||
const playlists = ref( [] );
|
||||
|
||||
const handlePlayerStateChange = ( newState: string ) => {
|
||||
if ( newState === 'hide' ) {
|
||||
@@ -26,21 +30,44 @@
|
||||
isShowingFullScreenPlayer.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
let loginChecker = 0;
|
||||
|
||||
const logIntoAppleMusic = () => {
|
||||
loginChecker = setInterval( () => {
|
||||
if ( player.value.getAuth()[ 0 ] ) {
|
||||
isLoggedIntoAppleMusic.value = true;
|
||||
player.value.getPlaylists( ( data ) => {
|
||||
console.log( data.data.data );
|
||||
playlists.value = data.data.data;
|
||||
} );
|
||||
clearInterval( loginChecker );
|
||||
} else if ( player.value.getAuth()[ 1 ] ) {
|
||||
clearInterval( loginChecker );
|
||||
alert( 'An error occurred when logging you in. Please try again!' );
|
||||
}
|
||||
}, 500 );
|
||||
}
|
||||
|
||||
const skipLogin = () => {
|
||||
isLoggedIntoAppleMusic.value = true;
|
||||
player.value.skipLogin();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.library-view {
|
||||
height: calc( 90vh - 10px );
|
||||
width: 100vw;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.app-view {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.home-view {
|
||||
height: 100vh;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.login-view {
|
||||
@@ -71,4 +98,8 @@
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.player-hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
17
backend/dist/app.js
vendored
17
backend/dist/app.js
vendored
@@ -24,15 +24,16 @@ const run = () => {
|
||||
// sign dev token
|
||||
const privateKey = fs_1.default.readFileSync(path_1.default.join(__dirname + '/config/apple_private_key.p8')).toString();
|
||||
// TODO: Remove secret
|
||||
const config = JSON.parse('' + fs_1.default.readFileSync(path_1.default.join(__dirname + '/config/apple-music-api.config.json')));
|
||||
const jwtToken = jsonwebtoken_1.default.sign({}, privateKey, {
|
||||
const config = JSON.parse('' + fs_1.default.readFileSync(path_1.default.join(__dirname + '/config/apple-music-api.config.secret.json')));
|
||||
const now = new Date().getTime();
|
||||
const tomorrow = now + 24 * 3600 * 1000;
|
||||
const jwtToken = jsonwebtoken_1.default.sign({
|
||||
'iss': config.teamID,
|
||||
'iat': Math.floor(now / 1000),
|
||||
'exp': Math.floor(tomorrow / 1000),
|
||||
}, privateKey, {
|
||||
algorithm: "ES256",
|
||||
expiresIn: "180d",
|
||||
issuer: config.teamID,
|
||||
header: {
|
||||
alg: "ES256",
|
||||
kid: config.keyID
|
||||
}
|
||||
keyid: config.keyID
|
||||
});
|
||||
res.send(jwtToken);
|
||||
});
|
||||
|
||||
@@ -26,15 +26,16 @@ const run = () => {
|
||||
// sign dev token
|
||||
const privateKey = fs.readFileSync( path.join( __dirname + '/config/apple_private_key.p8' ) ).toString();
|
||||
// TODO: Remove secret
|
||||
const config = JSON.parse( '' + fs.readFileSync( path.join( __dirname + '/config/apple-music-api.config.json' ) ) );
|
||||
const jwtToken = jwt.sign( {}, privateKey, {
|
||||
const config = JSON.parse( '' + fs.readFileSync( path.join( __dirname + '/config/apple-music-api.config.secret.json' ) ) );
|
||||
const now = new Date().getTime();
|
||||
const tomorrow = now + 24 * 3600 * 1000;
|
||||
const jwtToken = jwt.sign( {
|
||||
'iss': config.teamID,
|
||||
'iat': Math.floor( now / 1000 ),
|
||||
'exp': Math.floor( tomorrow / 1000 ),
|
||||
}, privateKey, {
|
||||
algorithm: "ES256",
|
||||
expiresIn: "180d",
|
||||
issuer: config.teamID,
|
||||
header: {
|
||||
alg: "ES256",
|
||||
kid: config.keyID
|
||||
}
|
||||
keyid: config.keyID
|
||||
} );
|
||||
res.send( jwtToken );
|
||||
} );
|
||||
|
||||
Reference in New Issue
Block a user