From 5b223875bdfa077e5f560ab15d6d17bacd2b5c75 Mon Sep 17 00:00:00 2001 From: Janis Hutz Date: Sun, 30 Aug 2026 19:58:08 +0200 Subject: [PATCH] feat: basic player commands --- web/src/ts/dtype/player.d.ts | 1 + web/src/ts/player/index.ts | 103 ++++++++++++++++++ web/src/ts/player/playlists/add.ts | 14 +++ web/src/ts/player/playlists/index.ts | 36 ++++++ web/src/ts/{ => player}/playlists/loader.ts | 1 + web/src/ts/player/plugins/interface.d.ts | 2 +- .../{queue.ts => plugins/local/loader.ts} | 0 .../plugins/musickit/search.ts} | 0 web/src/ts/player/state.ts | 14 ++- web/src/ts/player/status-tracking.ts | 4 + web/todo.md | 23 ++++ 11 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 web/src/ts/dtype/player.d.ts create mode 100644 web/src/ts/player/playlists/add.ts create mode 100644 web/src/ts/player/playlists/index.ts rename web/src/ts/{ => player}/playlists/loader.ts (76%) rename web/src/ts/player/{queue.ts => plugins/local/loader.ts} (100%) rename web/src/ts/{playlists/index.ts => player/plugins/musickit/search.ts} (100%) create mode 100644 web/src/ts/player/status-tracking.ts create mode 100644 web/todo.md diff --git a/web/src/ts/dtype/player.d.ts b/web/src/ts/dtype/player.d.ts new file mode 100644 index 0000000..1a3fe4f --- /dev/null +++ b/web/src/ts/dtype/player.d.ts @@ -0,0 +1 @@ +export type RepeatMode = 'off' | 'one' | 'all'; diff --git a/web/src/ts/player/index.ts b/web/src/ts/player/index.ts index e69de29..ffa36d3 100644 --- a/web/src/ts/player/index.ts +++ b/web/src/ts/player/index.ts @@ -0,0 +1,103 @@ +import { + currentSource, + queueIdx, + repeat, + shuffle, + sources +} from './state'; +import { + startTracking, + stopTracking +} from './status-tracking'; +import type { + RepeatMode +} from '../dtype/player'; +import { + addSongList +} from './playlists/add'; +import { + playIndex +} from './playlists'; + +const next = () => { + playIndex( queueIdx.value + 1 ); +}; + +const prev = () => { + // TODO: Back to beginning if more than some seconds have passed? + playIndex( queueIdx.value - 1 ); +}; + +const play = () => { + if ( currentSource.value === '' ) return; + + sources[currentSource.value]?.play(); + startTracking(); +}; + +const pause = () => { + if ( currentSource.value === '' ) return; + + sources[currentSource.value]?.pause(); + stopTracking(); +}; + +/** + * Seek to a specific point in the song + * @param pos - Percentage of song, value in [0, 1] + */ +const seekTo = ( pos: number ) => { + if ( currentSource.value === '' ) return; + + sources[currentSource.value]?.seekTo( pos ); +}; + +const skip10 = () => { + if ( currentSource.value === '' ) return; + + const source = sources[currentSource.value]; + const duration = source?.getDuration() ?? -1; + + seekTo( ( ( ( source?.getPlaybackPos() ?? 0 ) * duration ) + 10 ) / duration ); +}; + +const back10 = () => { + if ( currentSource.value === '' ) return; + + const source = sources[currentSource.value]; + const duration = source?.getDuration() ?? -1; + + seekTo( ( ( ( source?.getPlaybackPos() ?? 0 ) * duration ) - 10 ) / duration ); +}; + +const setShuffle = ( enabled: boolean ) => { + // TODO: Shuffle the order + shuffle.value = enabled; +}; + +const setRepeat = ( mode: RepeatMode ) => { + repeat.value = mode; +}; + +const getSources = (): string[] => { + return Object.keys( sources ); +}; + +const addSongFromSource = async ( source: string ) => { + addSongList( await sources[source]!.addSongsFromThisSource() ); +}; + +export default { + play, + pause, + seekTo, + skip10, + back10, + setShuffle, + setRepeat, + playIndex, + getSources, + addSongFromSource, + next, + prev +}; diff --git a/web/src/ts/player/playlists/add.ts b/web/src/ts/player/playlists/add.ts new file mode 100644 index 0000000..2c60f73 --- /dev/null +++ b/web/src/ts/player/playlists/add.ts @@ -0,0 +1,14 @@ +import type { + Song +} from '@/ts/dtype/playlist'; +import { + queue +} from '../state'; + +export const addSongList = ( songs: Song[] ) => { + for ( const song of songs ) { + queue.value.push( song ); + } +}; + +export const shuffleList = () => {}; diff --git a/web/src/ts/player/playlists/index.ts b/web/src/ts/player/playlists/index.ts new file mode 100644 index 0000000..4d24860 --- /dev/null +++ b/web/src/ts/player/playlists/index.ts @@ -0,0 +1,36 @@ +import { + currentSource, + queue, + queueIdx, + repeat, + sources +} from '../state'; +import { + startTracking, + stopTracking +} from '../status-tracking'; + +/** + * Play a song at the given index of the queue. Wraps to 0 and end if index below 0 or above end + * @param idx - The index in the queue to play at + */ +export const playIndex = async ( idx: number ) => { + if ( idx >= queue.value.length ) { + idx = repeat.value === 'all' ? 0 : -1; + } else if ( idx < 0 ) { + idx = repeat.value === 'all' ? queue.value.length - 1 : -1; + } + + if ( idx < 0 ) return false; + + sources[currentSource.value]?.stop(); + stopTracking(); + + const nextSong = queue.value[ idx ]!; + + queueIdx.value = idx; + currentSource.value = nextSong.source; + sources[currentSource.value]?.playSong( nextSong.identifier ); + + startTracking(); +}; diff --git a/web/src/ts/playlists/loader.ts b/web/src/ts/player/playlists/loader.ts similarity index 76% rename from web/src/ts/playlists/loader.ts rename to web/src/ts/player/playlists/loader.ts index 6c93486..a52fb73 100644 --- a/web/src/ts/playlists/loader.ts +++ b/web/src/ts/player/playlists/loader.ts @@ -1 +1,2 @@ // TODO: If local files contained, prompt user to select files, then associate based on file names +export const load = () => {}; diff --git a/web/src/ts/player/plugins/interface.d.ts b/web/src/ts/player/plugins/interface.d.ts index f004ec2..a17f9f2 100644 --- a/web/src/ts/player/plugins/interface.d.ts +++ b/web/src/ts/player/plugins/interface.d.ts @@ -22,7 +22,7 @@ export interface PlayerSourcePlugin { 'login'?: () => Promise; /** - * Play a song by its ID + * Play a song by its ID. Should start playing it immediately */ 'playSong': ( id: string ) => Promise; diff --git a/web/src/ts/player/queue.ts b/web/src/ts/player/plugins/local/loader.ts similarity index 100% rename from web/src/ts/player/queue.ts rename to web/src/ts/player/plugins/local/loader.ts diff --git a/web/src/ts/playlists/index.ts b/web/src/ts/player/plugins/musickit/search.ts similarity index 100% rename from web/src/ts/playlists/index.ts rename to web/src/ts/player/plugins/musickit/search.ts diff --git a/web/src/ts/player/state.ts b/web/src/ts/player/state.ts index 51c8810..25396dc 100644 --- a/web/src/ts/player/state.ts +++ b/web/src/ts/player/state.ts @@ -2,9 +2,21 @@ import { type Ref, ref } from 'vue'; +import type { + PlayerSourcePlugin +} from './plugins/interface'; import type { Playlist } from '../dtype/playlist'; +import type { + RepeatMode +} from '../dtype/player'; + +export const sources: { + [key: string]: PlayerSourcePlugin +} = {}; + +export const currentSource = ref( '' ); export const queueIdx = ref( 0 ); @@ -14,7 +26,7 @@ export const isPlaying = ref( false ); export const shuffle = ref( false ); -export const repeat: Ref<'off' | 'one' | 'all'> = ref( 'off' ); +export const repeat: Ref = ref( 'off' ); export const playbackPercentage = ref( 0 ); diff --git a/web/src/ts/player/status-tracking.ts b/web/src/ts/player/status-tracking.ts new file mode 100644 index 0000000..eb0f4fa --- /dev/null +++ b/web/src/ts/player/status-tracking.ts @@ -0,0 +1,4 @@ +// Tracking of time and player status +export const startTracking = () => {}; + +export const stopTracking = () => {}; diff --git a/web/todo.md b/web/todo.md new file mode 100644 index 0000000..bd7af45 --- /dev/null +++ b/web/todo.md @@ -0,0 +1,23 @@ +# Frontend +- [ ] Status tracking (position, etc) +- [ ] Change metadata association for automated association +- [ ] Move songs with D&D, or index assignment? +- [ ] Explain additional info +- [ ] Tour +- [ ] Get player working +- [ ] Save playlists locally or on backend +- [ ] SSE connection +- [ ] Anti-Tamper +- [ ] Remote showcase views +- [ ] Login +- [ ] About page +- [ ] Apple-Music signin +- [ ] Loading existing playlists with local music and association of that + +# Backend +- [ ] Implement all endpoints +- [ ] Integrate with store and auth sdks +- [ ] Create FOSS solution for above (single user) +- [ ] Setup guide +- [ ] Docker containers +- [ ] CI