refactor!: use plugin-based system for sources

This commit is contained in:
2026-08-29 17:25:40 +02:00
parent 4c5e1a864b
commit ce35371c06
11 changed files with 221 additions and 50 deletions
+2
View File
@@ -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
+2 -2
View File
@@ -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"
+6 -1
View File
@@ -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
*/
-35
View File
@@ -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 {};
};
-9
View File
@@ -1,9 +0,0 @@
export const play = () => {};
export const pause = () => {};
export const skip10 = () => {};
export const back10 = () => {};
export const playSong = ( id: string ) => {};
-3
View File
@@ -1,3 +0,0 @@
export const login = async () => {
};
+66
View File
@@ -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<PlayerSourcePlugin>;
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<boolean>;
/**
* Play a song by its ID
*/
'playSong': ( id: string ) => Promise<void>;
/**
* 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<Song[]>;
}
+27
View File
@@ -0,0 +1,27 @@
import type {
PlayerSourcePlugin,
PlayerSourcePluginInitializer
} from '../interface';
export const useLocalPlayer: PlayerSourcePluginInitializer = async (): Promise<PlayerSourcePlugin> => {
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 () => []
};
};
@@ -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<PlayerSourcePlugin> => {
return new Promise( ( resolve, reject ) => {
const init = async ( storefront: string ): Promise<PlayerSourcePlugin> => {
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<boolean> => {
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 );
}
} );
};
@@ -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
};
};
+1
View File
@@ -0,0 +1 @@
// TODO: If local files contained, prompt user to select files, then associate based on file names