diff --git a/README.md b/README.md index 2cdc411..0561829 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ # Development Branch This branch is for the upcoming version 4 of Music Player and is in early stages of development +It uses my manually created [types for MusicKitJS](https://github.com/janishutz/musickit-v3-types) + # MusicPlayer diff --git a/web/package.json b/web/package.json index 76b60ba..b41f298 100644 --- a/web/package.json +++ b/web/package.json @@ -19,7 +19,6 @@ "@eslint/js": "^10.0.1", "@stylistic/eslint-plugin": "^5.10.0", "@tsconfig/node24": "^24.0.4", - "@types/musickit-js": "^1.0.11", "@types/node": "^24.13.3", "@vitejs/plugin-vue": "^6.0.8", "@vue/tsconfig": "^0.9.1", @@ -31,7 +30,8 @@ "typescript-eslint": "^8.67.0", "vite": "^8.1.5", "vite-plugin-vue-devtools": "^8.1.5", - "vue-tsc": "^3.3.7" + "vue-tsc": "^3.3.7", + "musickitjs-v3-types": "^1.0.0" }, "engines": { "node": "^22.18.0 || >=24.12.0" diff --git a/web/src/ts/dtype/playlist.d.ts b/web/src/ts/dtype/playlist.d.ts index 68d34f5..3cbf36f 100644 --- a/web/src/ts/dtype/playlist.d.ts +++ b/web/src/ts/dtype/playlist.d.ts @@ -13,10 +13,15 @@ export interface Song { 'source': 'local' | 'applemusic'; /** - * The identifier for the song so it can be found again (such as Apple Music ID or filename) + * The identifier for the song so it can be found again (such as Apple Music ID or URL for playback) */ 'identifier': string; + /** + * Any additional identifiers (such as filename, etc) to associate + */ + 'additional-identifier'?: string; + /** * Additional info, such as dance type to be displayed on remote screens */ diff --git a/web/src/ts/player/musickit/index.ts b/web/src/ts/player/musickit/index.ts deleted file mode 100644 index fcd3808..0000000 --- a/web/src/ts/player/musickit/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -export const useMusicKit = ( storefront: string = 'ch' ) => { - const init = async () => { - const res = await fetch( import.meta.env.VITE_BACKEND_URL + '/', { - 'credentials': 'include' - } ); - - if ( res.status === 200 ) { - const token = await res.text(); - - await window.MusicKit.configure( { - 'developerToken': token, - 'app': { - 'name': 'MusicPlayer', - 'build': '4', - 'icon': 'https://music.janishutz.com/logo.jpg' - }, - 'storefront': storefront.toUpperCase() - } ); - } - - isInit = true; - }; - - let isInit = false; - - if ( !window.MusicKit ) { - document.addEventListener( 'musickitloaded', () => { - init(); - } ); - } else { - init(); - } - - return {}; -}; diff --git a/web/src/ts/player/musickit/playback.ts b/web/src/ts/player/musickit/playback.ts deleted file mode 100644 index e1137eb..0000000 --- a/web/src/ts/player/musickit/playback.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const play = () => {}; - -export const pause = () => {}; - -export const skip10 = () => {}; - -export const back10 = () => {}; - -export const playSong = ( id: string ) => {}; diff --git a/web/src/ts/player/musickit/user.ts b/web/src/ts/player/musickit/user.ts deleted file mode 100644 index d51e1eb..0000000 --- a/web/src/ts/player/musickit/user.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const login = async () => { - -}; diff --git a/web/src/ts/player/plugins/interface.d.ts b/web/src/ts/player/plugins/interface.d.ts new file mode 100644 index 0000000..f004ec2 --- /dev/null +++ b/web/src/ts/player/plugins/interface.d.ts @@ -0,0 +1,66 @@ +import type { + Song +} from '@/ts/dtype/playlist'; + +// TODO: Add search interface elements to plugin's args (such as a search interface) +export type PlayerSourcePluginInitializer = () => Promise; + +export interface PlayerSourcePlugin { + /** + * The name of the source that is displayed in the UI + */ + 'name': string; + + /** + * The identifier used in the playlist files + */ + 'id': string; + + /** + * Implement any login flow here. Optional + */ + 'login'?: () => Promise; + + /** + * Play a song by its ID + */ + 'playSong': ( id: string ) => Promise; + + /** + * Continue playing the song + */ + 'play': () => void; + + /** + * Pause the currently playing song + */ + 'pause': () => void; + + /** + * Stop / unload the current song. This indicates that either the playlist was cleared + * or a next song is about to be loaded (possibly from another plugin) + */ + 'stop': () => void; + + /** + * Seek to a specific point in the song. + * @param pos is always given as a percentage (pos \in [0, 1]) + */ + 'seekTo': ( pos: number ) => void; + + /** + * Return the playback position as a percentage of the duration. (return value \in [0, 1]) + */ + 'getPlaybackPos': () => number; + + /** + * Return the song duration in seconds. + */ + 'getDuration': () => number; + + /** + * Called when user adds another song to the playlist via this source. + * You may use the provided interface elements (search bar and popups) to e.g. ask if user wants to use a playlist, album, etc + */ + 'addSongsFromThisSource': () => Promise; +} diff --git a/web/src/ts/player/plugins/local/index.ts b/web/src/ts/player/plugins/local/index.ts new file mode 100644 index 0000000..5335d74 --- /dev/null +++ b/web/src/ts/player/plugins/local/index.ts @@ -0,0 +1,27 @@ +import type { + PlayerSourcePlugin, + PlayerSourcePluginInitializer +} from '../interface'; + +export const useLocalPlayer: PlayerSourcePluginInitializer = async (): Promise => { + const player = document.createElement( 'audio' ); + + const playSong = async ( id: string ) => { + player.src = id; + player.play(); + }; + + return { + 'id': 'local', + 'name': 'Local Disk', + 'play': player.play, + 'getPlaybackPos': () => player.currentTime / player.duration, + 'getDuration': () => player.duration, + playSong, + 'seekTo': pos => player.currentTime = pos, + 'pause': player.pause, + 'stop': () => player.src = '', + // TODO: Implement + 'addSongsFromThisSource': async () => [] + }; +}; diff --git a/web/src/ts/player/plugins/musickit/index.ts b/web/src/ts/player/plugins/musickit/index.ts new file mode 100644 index 0000000..159af3f --- /dev/null +++ b/web/src/ts/player/plugins/musickit/index.ts @@ -0,0 +1,71 @@ +import 'musickitjs-v3-types'; +import type { + PlayerSourcePlugin, + PlayerSourcePluginInitializer +} from '../interface'; +import { + musicKitPlayback +} from './playback'; + +export const useMusicKit: PlayerSourcePluginInitializer = ( storefront: string = 'ch' ): Promise => { + return new Promise( ( resolve, reject ) => { + const init = async ( storefront: string ): Promise => { + const res = await fetch( import.meta.env.VITE_BACKEND_URL + '/', { + 'credentials': 'include' + } ); + + if ( res.status === 200 ) { + const token = await res.text(); + const instance = await window.MusicKit.configure( { + 'developerToken': token, + 'app': { + 'name': 'MusicPlayer', + 'build': '4', + 'icon': 'https://music.janishutz.com/logo.jpg' + }, + 'storefrontId': storefront.toUpperCase() + } ); + const controls = musicKitPlayback( instance ); + + const login = async (): Promise => { + try { + await instance.authorize(); + + return true; + } catch { + return false; + } + }; + + return { + 'play': controls.play, + 'pause': controls.pause, + 'stop': controls.stop, + 'playSong': controls.playSong, + 'getDuration': controls.getDuration, + 'getPlaybackPos': controls.getPlaybackPos, + 'id': 'applemusic', + 'name': 'Apple Music', + 'seekTo': controls.seekTo, + 'login': login, + // TODO: Implement + 'addSongsFromThisSource': async () => [] + }; + } else { + throw new Error( 'ERR_AUTH' ); + } + }; + + if ( !window.MusicKit ) { + document.addEventListener( 'musickitloaded', () => { + init( storefront ) + .then( resolve ) + .catch( reject ); + } ); + } else { + init( storefront ) + .then( resolve ) + .catch( reject ); + } + } ); +}; diff --git a/web/src/ts/player/plugins/musickit/playback.ts b/web/src/ts/player/plugins/musickit/playback.ts new file mode 100644 index 0000000..261b4b4 --- /dev/null +++ b/web/src/ts/player/plugins/musickit/playback.ts @@ -0,0 +1,46 @@ +import type { + MusicKitInstance +} from 'musickitjs-v3-types/MusicKitInstance'; + +export const musicKitPlayback = ( musickitInstance: MusicKitInstance ) => { + const play = () => { + musickitInstance.play(); + }; + + const pause = () => { + musickitInstance.pause(); + }; + + const seekTo = ( pos: number ) => { + musickitInstance.seekToTime( pos * musickitInstance.currentPlaybackDuration ); + }; + + const playSong = async ( id: string ) => { + await musickitInstance.setQueue( { + 'song': id + } ); + musickitInstance.play(); + }; + + const stop = () => { + musickitInstance.stop(); + }; + + const getPlaybackPos = (): number => { + return musickitInstance.currentPlaybackProgress; + }; + + const getDuration = (): number => { + return musickitInstance.currentPlaybackDuration; + }; + + return { + play, + pause, + seekTo, + playSong, + stop, + getPlaybackPos, + getDuration + }; +}; diff --git a/web/src/ts/playlists/loader.ts b/web/src/ts/playlists/loader.ts new file mode 100644 index 0000000..6c93486 --- /dev/null +++ b/web/src/ts/playlists/loader.ts @@ -0,0 +1 @@ +// TODO: If local files contained, prompt user to select files, then associate based on file names