technically working player

This commit is contained in:
2024-06-25 14:17:04 +02:00
parent 1ffdc873a7
commit b2d8180bb9
7 changed files with 365 additions and 69 deletions

View File

@@ -1,13 +1,19 @@
<template> <template>
<div> <div>
<h1>Library</h1> <h1>Library</h1>
<playlistsView :playlists="$props.playlists"></playlistsView> <playlistsView :playlists="$props.playlists" @selected-playlist="( id ) => selectPlaylist( id )"></playlistsView>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import playlistsView from '@/components/playlistsView.vue'; import playlistsView from '@/components/playlistsView.vue';
const emits = defineEmits( [ 'selected-playlist' ] );
const selectPlaylist = ( id: string ) => {
emits( 'selected-playlist', id );
}
defineProps( { defineProps( {
'playlists': { 'playlists': {
'default': [], 'default': [],

View File

@@ -2,8 +2,10 @@
<div> <div>
<div :class="'player' + ( isShowingFullScreenPlayer ? '' : ' player-hidden' )"> <div :class="'player' + ( isShowingFullScreenPlayer ? '' : ' player-hidden' )">
<!-- TODO: Make cover art of song or otherwise MusicPlayer Logo --> <!-- TODO: Make cover art of song or otherwise MusicPlayer Logo -->
<img src="https://github.com/simplePCBuilding/MusicPlayerV2/raw/master/assets/logo.png" alt="MusicPlayer Logo" class="logo-player" @click="controlUI( 'show' )"> <img src="https://github.com/simplePCBuilding/MusicPlayerV2/raw/master/assets/logo.png" alt="MusicPlayer Logo" class="logo-player" @click="controlUI( 'show' )" v-if="coverArt === ''">
<img :src="coverArt" alt="MusicPlayer Logo" class="logo-player" @click="controlUI( 'show' )" v-else>
<p class="song-name" @click="controlUI( 'show' )">{{ currentlyPlayingSongName }}</p> <p class="song-name" @click="controlUI( 'show' )">{{ currentlyPlayingSongName }}</p>
<p>{{ nicePlaybackPos }} / {{ niceDuration }}</p>
<div class="controls-wrapper"> <div class="controls-wrapper">
<span class="material-symbols-outlined controls next-previous" @click="control( 'previous' )" id="previous">skip_previous</span> <span class="material-symbols-outlined controls next-previous" @click="control( 'previous' )" id="previous">skip_previous</span>
<span class="material-symbols-outlined controls forward-back" @click="control( 'back' )" :style="'rotate: -' + 360 * clickCountBack + 'deg;'">replay_10</span> <span class="material-symbols-outlined controls forward-back" @click="control( 'back' )" :style="'rotate: -' + 360 * clickCountBack + 'deg;'">replay_10</span>
@@ -18,7 +20,7 @@
</div> </div>
<div :class="'playlist-view' + ( isShowingFullScreenPlayer ? '' : ' hidden' )"> <div :class="'playlist-view' + ( isShowingFullScreenPlayer ? '' : ' hidden' )">
<span class="material-symbols-outlined close-fullscreen" @click="controlUI( 'hide' )">close</span> <span class="material-symbols-outlined close-fullscreen" @click="controlUI( 'hide' )">close</span>
<playlistView></playlistView> <playlistView :playlist="playlist" class="pl-wrapper"></playlistView>
</div> </div>
</div> </div>
</template> </template>
@@ -27,9 +29,10 @@
<script setup lang="ts"> <script setup lang="ts">
// TODO: Handle resize, hide all non-essential controls when below 900px width // TODO: Handle resize, hide all non-essential controls when below 900px width
import { ref } from 'vue'; import { ref, type Ref } from 'vue';
import playlistView from '@/components/playlistView.vue'; import playlistView from '@/components/playlistView.vue';
import MusicKitJSWrapper from '@/scripts/music-player'; import MusicKitJSWrapper from '@/scripts/music-player';
import type { Song } from '@/scripts/song';
const isPlaying = ref( false ); const isPlaying = ref( false );
const repeatMode = ref( '' ); const repeatMode = ref( '' );
@@ -39,6 +42,11 @@
const clickCountBack = ref( 0 ); const clickCountBack = ref( 0 );
const isShowingFullScreenPlayer = ref( false ); const isShowingFullScreenPlayer = ref( false );
const player = new MusicKitJSWrapper(); const player = new MusicKitJSWrapper();
const playlist: Ref<Song[]> = ref( [] );
const coverArt = ref( '' );
const nicePlaybackPos = ref( '' );
const niceDuration = ref( '' );
const isShowingRemainingTime = ref( false );
const emits = defineEmits( [ 'playerStateChange' ] ); const emits = defineEmits( [ 'playerStateChange' ] );
@@ -47,8 +55,10 @@
// TODO: Execute function on player // TODO: Execute function on player
if ( isPlaying.value ) { if ( isPlaying.value ) {
player.control( 'play' ); player.control( 'play' );
startProgressTracker();
} else { } else {
player.control( 'pause' ); player.control( 'pause' );
stopProgressTracker();
} }
} }
@@ -56,21 +66,44 @@
if ( action === 'repeat' ) { if ( action === 'repeat' ) {
if ( repeatMode.value === '' ) { if ( repeatMode.value === '' ) {
repeatMode.value = '_on'; repeatMode.value = '_on';
player.setRepeatMode( 'all' );
} else if ( repeatMode.value === '_on' ) { } else if ( repeatMode.value === '_on' ) {
repeatMode.value = '_one_on'; repeatMode.value = '_one_on';
player.setRepeatMode( 'once' );
} else { } else {
repeatMode.value = ''; repeatMode.value = '';
player.setRepeatMode( 'off' );
} }
} else if ( action === 'shuffle' ) { } else if ( action === 'shuffle' ) {
if ( shuffleMode.value === '' ) { if ( shuffleMode.value === '' ) {
shuffleMode.value = '_on'; shuffleMode.value = '_on';
player.setShuffle( true );
} else { } else {
shuffleMode.value = ''; shuffleMode.value = '';
player.setShuffle( false );
} }
} else if ( action === 'forward' ) { } else if ( action === 'forward' ) {
clickCountForward.value += 1; clickCountForward.value += 1;
player.control( 'skip-10' );
} else if ( action === 'back' ) { } else if ( action === 'back' ) {
clickCountBack.value += 1; clickCountBack.value += 1;
player.control( 'back-10' );
} else if ( action === 'next' ) {
stopProgressTracker();
player.control( 'next' );
currentlyPlayingSongName.value = 'Loading...';
setTimeout( () => {
getDetails();
startProgressTracker();
}, 2000 );
} else if ( action === 'previous' ) {
stopProgressTracker();
player.control( 'previous' );
currentlyPlayingSongName.value = 'Loading...';
setTimeout( () => {
getDetails();
startProgressTracker();
}, 2000 );
} }
} }
@@ -101,12 +134,87 @@
player.init(); player.init();
} }
const selectPlaylist = ( id: string ) => {
currentlyPlayingSongName.value = 'Loading...';
player.setPlaylistByID( id ).then( () => {
isPlaying.value = true;
setTimeout( () => {
startProgressTracker();
getDetails();
}, 2000 );
} );
}
const getDetails = () => {
const details = player.getPlayingSong();
currentlyPlayingSongName.value = details.title;
coverArt.value = details.cover;
// console.log( player.getQueue() );
playlist.value = player.getPlaylist();
}
let progressTracker = 0;
const startProgressTracker = () => {
const playingSong = player.getPlayingSong();
const minuteCounts = Math.floor( ( playingSong.duration ) / 60 );
niceDuration.value = String( minuteCounts ) + ':';
if ( ( '' + minuteCounts ).length === 1 ) {
niceDuration.value = '0' + minuteCounts + ':';
}
const secondCounts = Math.floor( ( playingSong.duration ) - minuteCounts * 60 );
if ( ( '' + secondCounts ).length === 1 ) {
niceDuration.value += '0' + secondCounts;
} else {
niceDuration.value += secondCounts;
}
progressTracker = setInterval( () => {
const pos = player.getPlaybackPos();
if ( pos > playingSong.duration - 1 ) {
control( 'next' );
}
const minuteCount = Math.floor( pos / 60 );
nicePlaybackPos.value = minuteCount + ':';
if ( ( '' + minuteCount ).length === 1 ) {
nicePlaybackPos.value = '0' + minuteCount + ':';
}
const secondCount = Math.floor( pos - minuteCount * 60 );
if ( ( '' + secondCount ).length === 1 ) {
nicePlaybackPos.value += '0' + secondCount;
} else {
nicePlaybackPos.value += secondCount;
}
if ( isShowingRemainingTime.value ) {
const minuteCounts = Math.floor( ( playingSong.duration - pos ) / 60 );
niceDuration.value = '-' + String( minuteCounts ) + ':';
if ( ( '' + minuteCounts ).length === 1 ) {
niceDuration.value = '-0' + minuteCounts + ':';
}
const secondCounts = Math.floor( ( playingSong.duration - pos ) - minuteCounts * 60 );
if ( ( '' + secondCounts ).length === 1 ) {
niceDuration.value += '0' + secondCounts;
} else {
niceDuration.value += secondCounts;
}
}
}, 50 );
}
const stopProgressTracker = () => {
try {
clearInterval( progressTracker );
} catch ( _ ) { /* empty */ }
}
defineExpose( { defineExpose( {
logIntoAppleMusic, logIntoAppleMusic,
getPlaylists, getPlaylists,
controlUI, controlUI,
getAuth, getAuth,
skipLogin, skipLogin,
selectPlaylist,
} ); } );
</script> </script>
@@ -141,7 +249,6 @@
} }
.playlist-view { .playlist-view {
height: 15%;
overflow: scroll; overflow: scroll;
} }
@@ -214,4 +321,8 @@
.hidden .close-fullscreen { .hidden .close-fullscreen {
display: none; display: none;
} }
.pl-wrapper {
height: 80vh;
}
</style> </style>

View File

@@ -1,5 +1,50 @@
<template> <template>
<div> <div>
<h1>Playlist</h1> <h1>Playlist</h1>
<div class="playlist-box">
<div class="song" v-for="song in $props.playlist" v-bind:key="song.id">
<img :src="song.cover" alt="Song cover" class="song-cover">
<h3 class="song-title">{{ song.title }}</h3>
<p class="playing-in">playing in</p>
</div>
</div>
</div> </div>
</template> </template>
<script setup lang="ts">
import type { Song } from '@/scripts/song';
defineProps( {
'playlist': {
default: [],
required: true,
type: Array<Song>
}
} );
</script>
<style scoped>
.playlist-box {
height: 80vh !important;
}
.song {
border: solid var( --primary-color ) 1px;
padding: 10px;
margin: 5px;
width: 50%;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.song-title {
margin-left: 10px;
margin-right: auto;
}
.song-cover {
height: 70px;
}
</style>

View File

@@ -1,18 +1,58 @@
<template> <template>
<div> <div class="playlists">
<h3>Your playlists</h3> <h3 style="width: fit-content;">Your playlists</h3>
<div v-for="pl in $props.playlists" v-bind:key="pl.id"> <div v-if="$props.playlists ? $props.playlists.length < 1 : true">
{{ pl.attributes.name }} loading...
<!-- TODO: Make prettier -->
</div>
<div class="playlist-wrapper">
<div v-for="pl in $props.playlists" v-bind:key="pl.id" class="playlist" @click="selectPlaylist( pl.id )">
{{ pl.attributes.name }}
</div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const props = defineProps( { defineProps( {
'playlists': { 'playlists': {
'default': [], 'default': [],
'type': Array<any>, 'type': Array<any>,
'required': true, 'required': true,
} }
} ) } );
</script>
const emits = defineEmits( [ 'selected-playlist' ] );
const selectPlaylist = ( id: string ) => {
emits( 'selected-playlist', id );
}
</script>
<style scoped>
.playlists {
width: 100%;
height: 75vh;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.playlist-wrapper {
width: 85%;
overflow-y: scroll;
overflow-x: hidden;
}
.playlist {
width: 100%;
padding: 15px;
border: solid var( --primary-color ) 1px;
border-radius: 5px;
margin: 1px;
cursor: pointer;
user-select: none;
}
</style>

View File

@@ -1,46 +1,4 @@
type Origin = 'apple-music' | 'disk'; import type { Song } from "./song";
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 { interface Config {
devToken: string; devToken: string;
@@ -61,6 +19,7 @@ class MusicKitJSWrapper {
repeatMode: RepeatMode; repeatMode: RepeatMode;
isShuffleEnabled: boolean; isShuffleEnabled: boolean;
hasEncounteredAuthError: boolean; hasEncounteredAuthError: boolean;
queuePos: number;
constructor () { constructor () {
this.playingSongID = 0; this.playingSongID = 0;
@@ -75,6 +34,7 @@ class MusicKitJSWrapper {
this.isPreparedToPlay = false; this.isPreparedToPlay = false;
this.isLoggedIn = false; this.isLoggedIn = false;
this.hasEncounteredAuthError = false; this.hasEncounteredAuthError = false;
this.queuePos = 0;
const self = this; const self = this;
@@ -187,6 +147,39 @@ class MusicKitJSWrapper {
this.setShuffle( this.isShuffleEnabled ); this.setShuffle( this.isShuffleEnabled );
} }
setPlaylistByID ( id: string ): Promise<void> {
return new Promise( ( resolve, reject ) => {
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
}
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 );
} );
} );
}
/** /**
* Prepare a specific song in the queue for playing and start playing * 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 * @param {number} playlistID The ID of the song in the playlist to prepare to play
@@ -196,6 +189,17 @@ class MusicKitJSWrapper {
if ( this.playlist.length > 0 ) { if ( this.playlist.length > 0 ) {
this.playingSongID = playlistID; this.playingSongID = playlistID;
this.isPreparedToPlay = true; this.isPreparedToPlay = true;
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
this.musicKit.setQueue( { 'song': this.playlist[ this.playingSongID ].id } ).then( () => {
setTimeout( () => {
this.control( 'play' );
}, 500 );
} ).catch( ( err ) => {
console.log( err );
} );
} else {
// TODO: Implement
}
return true; return true;
} else { } else {
return false; return false;
@@ -205,48 +209,54 @@ class MusicKitJSWrapper {
/** /**
* Control the player * Control the player
* @param {ControlAction} action Action to take on the player * @param {ControlAction} action Action to take on the player
* @returns {void} * @returns {boolean} returns a boolean indicating if there was a change in song.
*/ */
control ( action: ControlAction ): void { control ( action: ControlAction ): boolean {
switch ( action ) { switch ( action ) {
case "play": case "play":
if ( this.isPreparedToPlay ) { if ( this.isPreparedToPlay ) {
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) { if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
this.musicKit.play(); this.musicKit.play();
return false;
} else { } else {
return false;
// TODO: Implement // TODO: Implement
} }
} else { } else {
return; return false;
} }
break;
case "pause": case "pause":
if ( this.isPreparedToPlay ) { if ( this.isPreparedToPlay ) {
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) { if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
this.musicKit.pause(); this.musicKit.pause();
return false;
} else { } else {
return false;
// TODO: Implement // TODO: Implement
} }
} else { } else {
return; return false;
} }
break;
case "back-10": case "back-10":
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) { if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
this.musicKit.seekToTime( this.musicKit.currentPlaybackTime > 10 ? this.musicKit.currentPlaybackTime - 10 : 0 ); this.musicKit.seekToTime( this.musicKit.currentPlaybackTime > 10 ? this.musicKit.currentPlaybackTime - 10 : 0 );
return false;
} else { } else {
return false;
// TODO: Implement // TODO: Implement
} }
break;
case "skip-10": case "skip-10":
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) { if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
if ( this.musicKit.currentPlaybackTime < ( this.playlist[ this.playingSongID ].duration - 10 ) ) { if ( this.musicKit.currentPlaybackTime < ( this.playlist[ this.playingSongID ].duration - 10 ) ) {
this.musicKit.seekToTime( this.musicKit.currentPlaybackTime + 10 ); this.musicKit.seekToTime( this.musicKit.currentPlaybackTime + 10 );
return false;
} else { } else {
if ( this.repeatMode !== 'once' ) { if ( this.repeatMode !== 'once' ) {
this.control( 'next' ); this.control( 'next' );
return true;
} else { } else {
this.musicKit.seekToTime( 0 ); this.musicKit.seekToTime( 0 );
return false;
} }
} }
} else { } else {
@@ -264,18 +274,47 @@ class MusicKitJSWrapper {
// this.sendUpdate( 'pos' ); // this.sendUpdate( 'pos' );
// } // }
// } // }
return false;
} }
break;
case "next": case "next":
// if ( this.queuePos < this.queue.length ) {
break; this.queuePos += 1;
this.prepare( this.queue[ this.queuePos ] );
return true;
} else {
this.queuePos = 0;
this.control( 'pause' );
return true;
}
case "previous": case "previous":
if ( this.queuePos > 0 ) {
this.queuePos -= 1;
this.prepare( this.queue[ this.queuePos ] );
return true;
} else {
this.queuePos = this.queue.length - 1;
return true;
}
} }
} }
setShuffle ( enabled: boolean ) { setShuffle ( enabled: boolean ) {
this.isShuffleEnabled = enabled; this.isShuffleEnabled = enabled;
// TODO: Shuffle playlist this.queue = [];
if ( enabled ) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for ( const _ in this.playlist ) {
let val = Math.floor( Math.random() * this.playlist.length );
while ( this.queue.includes( val ) ) {
val = Math.floor( Math.random() * this.playlist.length );
}
this.queue.push( val );
}
} else {
for ( const song in this.playlist ) {
this.queue.push( parseInt( song ) );
}
}
} }
setRepeatMode ( mode: RepeatMode ) { setRepeatMode ( mode: RepeatMode ) {
@@ -353,6 +392,15 @@ class MusicKitJSWrapper {
} }
} }
getPlaying ( ): boolean {
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
return this.musicKit.isPlaying;
} else {
// TODO: Implement
return false;
}
}
// findSongOnAppleMusic ( searchTerm: string ): Song => { // findSongOnAppleMusic ( searchTerm: string ): Song => {
// TODO: Implement // TODO: Implement
// } // }

43
MusicPlayerV2-GUI/src/scripts/song.d.ts vendored Normal file
View File

@@ -0,0 +1,43 @@
export type Origin = 'apple-music' | 'disk';
export 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;
}

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="app-view"> <div class="app-view">
<div class="home-view" v-if="isLoggedIntoAppleMusic"> <div class="home-view" v-if="isLoggedIntoAppleMusic">
<libraryView class="library-view" :playlists="playlists"></libraryView> <libraryView class="library-view" :playlists="playlists" @selected-playlist="( id ) => { selectPlaylist( id ) }"></libraryView>
</div> </div>
<div v-else class="login-view"> <div v-else class="login-view">
<img src="@/assets/appleMusicIcon.svg" alt="Apple Music Icon"> <img src="@/assets/appleMusicIcon.svg" alt="Apple Music Icon">
@@ -38,7 +38,6 @@
if ( player.value.getAuth()[ 0 ] ) { if ( player.value.getAuth()[ 0 ] ) {
isLoggedIntoAppleMusic.value = true; isLoggedIntoAppleMusic.value = true;
player.value.getPlaylists( ( data ) => { player.value.getPlaylists( ( data ) => {
console.log( data.data.data );
playlists.value = data.data.data; playlists.value = data.data.data;
} ); } );
clearInterval( loginChecker ); clearInterval( loginChecker );
@@ -53,6 +52,10 @@
isLoggedIntoAppleMusic.value = true; isLoggedIntoAppleMusic.value = true;
player.value.skipLogin(); player.value.skipLogin();
} }
const selectPlaylist = ( id: string ) => {
player.value.selectPlaylist( id );
}
</script> </script>
<style scoped> <style scoped>