feat: redesign main page, get player working (mostly) with Apple Music source

This commit is contained in:
2026-09-01 18:16:11 +02:00
parent faa271edd6
commit 01526034ce
29 changed files with 425 additions and 219 deletions
+22 -2
View File
@@ -23,6 +23,9 @@ import {
import type {
RepeatMode
} from '../dtype/player';
import type {
Song
} from '../dtype/playlist';
import {
playIndex
} from './playlists';
@@ -118,12 +121,29 @@ const getSources = (): string[] => {
return Object.keys( sources );
};
const addToSongList = ( songs: Song[] ) => {
addSongList( songs );
if ( currentSource.value === '' ) {
playIndex( 0 );
}
};
/**
* Add songs to the playlist from given source
* @param source - The ID of the source to add from
*/
const addSongFromSource = async ( source: string ) => {
sources[source]!.addSongsFromThisSource( addSongList );
const addSongFromSource = async ( source: string ): Promise<boolean> => {
if ( sources[source]!.authorized.value ) {
sources[source]!.addSongsFromThisSource( addToSongList );
} else {
// TODO: Make this interact with user
sources[source]!.login!();
return false;
}
return true;
};
export default {
+12 -1
View File
@@ -1,9 +1,16 @@
import {
currentSource,
isPlaying,
queue,
queueIdx,
rawQueue,
shuffle
shuffle,
sources
} from '../state';
import {
duration,
playbackPercentage
} from '../status-tracking';
import type {
Song
} from '@/ts/dtype/playlist';
@@ -23,6 +30,10 @@ export const addSongList = ( songs: Song[], first: boolean = false ) => {
export const clearQueue = () => {
queue.value = [];
rawQueue.value = [];
sources[currentSource.value]?.stop();
duration.value = -1;
playbackPercentage.value = 1;
isPlaying.value = false;
};
/**
+11 -2
View File
@@ -1,5 +1,6 @@
import {
currentSource,
isPlaying,
queue,
queueIdx,
repeat,
@@ -21,9 +22,15 @@ export const playIndex = async ( idx: number ) => {
idx = repeat.value === 'all' ? queue.value.length - 1 : -1;
}
if ( idx < 0 ) return false;
sources[currentSource.value]?.stop();
if ( idx < 0 ) {
isPlaying.value = false;
currentSource.value = '';
return false;
}
stopTracking();
const nextSong = queue.value[ idx ]!;
@@ -33,4 +40,6 @@ export const playIndex = async ( idx: number ) => {
sources[currentSource.value]?.playSong( nextSong.identifier );
startTracking();
isPlaying.value = true;
};
+5 -2
View File
@@ -1,3 +1,6 @@
import type {
Ref
} from 'vue';
import type {
Song
} from '@/ts/dtype/playlist';
@@ -91,9 +94,9 @@ export interface PlayerSourcePlugin {
'getDuration': () => number;
/**
* Check if source is available for playback
* Check if source is authorized
*/
'available': () => boolean;
'authorized': Ref<boolean>;
/**
* Called when user adds another song to the playlist via this source.
+4 -1
View File
@@ -2,6 +2,9 @@ import type {
PlayerSourcePlugin,
PlayerSourcePluginInitializer
} from '../interface';
import {
ref
} from 'vue';
export const useLocalPlayer: PlayerSourcePluginInitializer = async (): Promise<PlayerSourcePlugin> => {
const player = document.createElement( 'audio' );
@@ -12,7 +15,7 @@ export const useLocalPlayer: PlayerSourcePluginInitializer = async (): Promise<P
};
return {
'available': () => true,
'authorized': ref( true ),
'id': 'local',
'name': 'Local Disk',
'play': player.play,
+1 -1
View File
@@ -43,7 +43,7 @@ export const useMusicKit: PlayerSourcePluginInitializer = ( storefront: string =
};
return {
'available': controls.getLoggedIn,
'authorized': controls.loggedIn,
'play': controls.play,
'pause': controls.pause,
'stop': controls.stop,
+10 -5
View File
@@ -1,6 +1,9 @@
import type {
MusicKitInstance
} from 'musickitjs-v3-types/MusicKitInstance';
import {
ref
} from 'vue';
export const musicKitPlayback = ( musickitInstance: MusicKitInstance ) => {
const play = () => {
@@ -27,16 +30,18 @@ export const musicKitPlayback = ( musickitInstance: MusicKitInstance ) => {
};
const getPlaybackPos = (): number => {
return musickitInstance.currentPlaybackProgress;
return musickitInstance.currentPlaybackTime / musickitInstance.currentPlaybackDuration;
};
const getDuration = (): number => {
return musickitInstance.currentPlaybackDuration;
};
const getLoggedIn = (): boolean => {
return musickitInstance.isAuthorized;
};
const loggedIn = ref( musickitInstance.isAuthorized );
musickitInstance.addEventListener( 'authorizationStatusDidChange', () => {
loggedIn.value = musickitInstance.isAuthorized;
} );
return {
play,
@@ -46,6 +51,6 @@ export const musicKitPlayback = ( musickitInstance: MusicKitInstance ) => {
stop,
getPlaybackPos,
getDuration,
getLoggedIn
loggedIn
};
};
+11 -1
View File
@@ -18,9 +18,19 @@ export interface AppleMusicSongData {
'attributes': {
'albumName': string;
'artistName': string;
'artwork': Artwork,
'artwork': Artwork;
'name': string;
'genreNames': string[];
'durationInMillis': number;
}
}
export interface AppleMusicPlaylistData {
'id': string;
'type': string;
'href': string;
'attributes': {
'artwork': Artwork;
'name': string;
}
}
@@ -1,3 +1,6 @@
import type {
AppleMusicPlaylistData
} from './dtype';
import type {
MusicKitInstance
} from 'musickitjs-v3-types/MusicKitInstance';
@@ -6,10 +9,33 @@ import type {
} from '@/ts/dtype/playlist';
export const searchPlaylists = async ( instance: MusicKitInstance, cb: ( songs: Song[] ) => void ) => {
// TODO: Get all playlists
let playlists: Song[] = [];
let filtered: Song[] = [];
const search = async ( term: string ): Promise<Song[]> => {
return [];
if ( playlists.length === 0 ) {
playlists = ( ( await instance.api.music( '/v1/me/library/playlists', {
'limit': 100
} ) ).data as {
'data': AppleMusicPlaylistData[]
} ).data.map( val => {
return {
'artist': 'Playlist',
'artwork': val.attributes.artwork ? window.MusicKit.formatArtworkURL( val.attributes.artwork, 1000, 100 ) : '',
'duration': 0,
'identifier': val.id,
'additional-info': '',
'name': val.attributes.name,
'source': 'applemusic'
};
} );
}
filtered = playlists.filter( val => {
return val.name.includes( term );
} );
return filtered;
};
const addSelected = async ( idx: number ) => {
@@ -1,5 +1,6 @@
import type {
AppleMusicApiSearchResult
AppleMusicApiSearchResult,
AppleMusicSongData
} from './dtype';
import type {
MusicKitInstance
@@ -16,7 +17,16 @@ export const searchSongs = async ( instance: MusicKitInstance, cb: ( songs: Song
'term': term,
'types': [ 'songs' ]
};
const results = ( ( await instance.api.music( '/v1/catalog/{{storefrontId}}/search', params ) ).data as AppleMusicApiSearchResult ).results.songs.data;
let results: AppleMusicSongData[] = [];
try {
results = ( ( await instance.api.music( '/v1/catalog/{{storefrontId}}/search', params ) ).data as AppleMusicApiSearchResult ).results.songs.data;
} catch {
console.debug( 'Failed results: got', results );
return [];
}
songs = [];
+6 -2
View File
@@ -24,12 +24,15 @@ export const startTracking = () => {
}
duration.value = sources[currentSource.value]?.getDuration() ?? -1;
setTimeout( () => {
duration.value = sources[currentSource.value]?.getDuration() ?? -1;
}, 2000 );
};
const tracker = () => {
playbackPercentage.value = sources[currentSource.value]?.getPlaybackPos() ?? -1;
if ( playbackPercentage.value > duration.value ) {
if ( playbackPercentage.value > 0.995 && duration.value > 0 ) {
if ( repeat.value === 'one' ) {
sources[currentSource.value]?.seekTo( 0 );
} else {
@@ -42,8 +45,9 @@ const tracker = () => {
export const stopTracking = () => {
try {
clearInterval( interval );
interval = -1;
} catch {
// empty
}
interval = -1;
};
+4 -4
View File
@@ -10,13 +10,13 @@ export const beautifyTime = ( time: number ): string => {
else if ( Math.floor( t / 60 ) > 0 )
return `${ helper( Math.floor( t / 60 ), depth + 1 ) }:${ t % 60 < 10 ? '0' : '' }${ Math.floor( t % 60 ) }`;
else
return `${ t % 60 < 10 ? '0' : '' }${ Math.floor( t % 60 ) }`;
return `${ depth === 1 ? '00:' : '' }${ t % 60 < 10 ? '0' : '' }${ Math.floor( t % 60 ) }`;
};
if ( time < 0 ) {
return '-:--';
if ( time < 0 || isNaN( time ) ) {
return '--:--';
} else if ( time == 0 ) {
return '0:00';
return '00:00';
}
return helper( Math.round( time ), 1 );