feat: improve sync

This commit is contained in:
2026-09-08 11:20:17 +02:00
parent 047a3a8b29
commit b11882c993
19 changed files with 160 additions and 55 deletions
+6 -3
View File
@@ -41,7 +41,8 @@ const create = ( name: string, uid: string ) => {
'index': 0,
'lastUpdate': new Date().getTime(),
'playing': false,
'start': new Date().getTime()
'start': new Date().getTime(),
'offset': 0
}
};
@@ -112,9 +113,10 @@ const close = ( name: string, uid: string ) => {
* @param playing - Set to true if the player is playing currently
* @param index - The current playback index
* @param start - The timestamp where playback of the current song started
* @param offset - The playback offset from the start in seconds
* @returns true if update succeeded
*/
const updateState = ( room: string, uid: string, playing: boolean, index: number, start: number ) => {
const updateState = ( room: string, uid: string, playing: boolean, index: number, start: number, offset: number ) => {
if ( !rooms[room] || rooms[room].owner !== uid ) {
return false;
}
@@ -123,7 +125,8 @@ const updateState = ( room: string, uid: string, playing: boolean, index: number
'playing': playing,
'index': index,
'lastUpdate': new Date().getTime(),
'start': start
'start': start,
'offset': offset
};
return sendUpdate( room, 'state' );
+1
View File
@@ -20,6 +20,7 @@ export interface Room {
'start': number;
'playing': boolean;
'lastUpdate': number;
'offset': number;
};
'owner': string;
'clients': Client[];
+2 -1
View File
@@ -79,7 +79,8 @@ const routes = ( app: express.Application, foss: boolean, config: Config ) => {
sdk.getUID( request )!,
request.body.playing ?? false,
request.body.index ?? -1,
request.body.start ?? new Date().getTime()
request.body.start ?? new Date().getTime(),
request.body.offset ?? 0
) )
response.sendStatus( 200 );
else
@@ -8,6 +8,7 @@
import {
beautifyTime
} from '@/ts/util/time';
import messages from '@/ts/messages';
import player from '@/ts/player';
const playbackPercentage = player.playbackPercentage;
@@ -42,6 +43,8 @@
const duration = computed( () => {
return beautifyTime( player.duration.value );
} );
messages.useRoomWatchers();
</script>
<template>
+6 -1
View File
@@ -12,8 +12,13 @@
const offset = ref( -1 );
const isMoving = ref( false );
const bar = useTemplateRef( 'bar' );
const props = defineProps<{
'disallowMove'?: boolean
}>();
const start = ( ev: MouseEvent ) => {
if ( props.disallowMove ) return;
offset.value = bar.value!.getBoundingClientRect().x;
isMoving.value = true;
moveVal.value = ( ev.x - offset.value ) / bar.value!.clientWidth;
@@ -47,7 +52,7 @@
<div :style="`width: ${ isMoving ? moveVal * 100 : val * 100 }%;`"></div>
</div>
<div
:class="['click-target', offset >= 0 ? 'active' : undefined]"
:class="['click-target', offset >= 0 ? 'active' : undefined, props.disallowMove ? 'disallowed' : undefined]"
@mousedown="start"
@mousemove="move"
@mouseup="end"
@@ -1,11 +1,11 @@
<script setup lang="ts">
import {
addPlaylist,
removePlaylist
removePlaylist,
selectPlaylist
} from '@/ts/userPlaylists';
import {
editingPlaylists,
playlistIdx as playlistIdx,
playlists
} from '@/ts/userPlaylists/state';
import {
@@ -20,7 +20,6 @@
import {
UnownedError
} from '@/ts/request';
import player from '@/ts/player';
import router from '@/router';
const checkingStatus = ref( true );
@@ -65,12 +64,6 @@
const togglePlaylistEditing = ( idx: number ) => {
editingPlaylists.value[ idx ] = !editingPlaylists.value[ idx ];
};
const selectPlaylist = ( idx: number ) => {
playlistIdx.value = idx;
player.clearQueue();
player.loadPlaylist( playlists.value[ idx ]!.songs );
};
</script>
<template>
+27 -25
View File
@@ -1,16 +1,15 @@
<script setup lang="ts">
import {
computed,
ref
} from 'vue';
import {
currentQueue,
currentQueueIdx
currentQueueIdx,
playbackTime,
showArtworks
} from '@/ts/shared/state';
import {
computed
} from 'vue';
const songs = computed( () => currentQueue.value?.slice( currentQueueIdx.value + 1 ) ?? [] );
// TODO: Move this out of this file
const showArtworks = ref( false );
const timeToPlay = computed( () => {
return ( idx: number ) => {
let total = 0;
@@ -20,6 +19,7 @@
}
total += currentQueue.value[ currentQueueIdx.value ]?.duration ?? 0;
total -= playbackTime.value;
return Math.round( total / 60 );
};
@@ -27,24 +27,26 @@
</script>
<template>
<div class="queue-container">
<div v-for="(song, index) in songs" :key="index" class="song-list-element">
<div class="song-cover-wrapper">
<img
v-if="song.artwork && showArtworks"
:src="song.artwork"
alt="Song cover"
class="song-cover"
>
<i v-else class="fa-solid fa-music song-cover"></i>
</div>
<div class="song-details">
<h3>{{ song.name }}</h3>
<p>{{ song.artist }}</p>
<p>{{ song['additional-info'] }}</p>
</div>
<div class="song-actions">
<p>In {{ timeToPlay( index ) }}min</p>
<div class="queue-viewer">
<div class="queue-container">
<div v-for="(song, index) in songs" :key="index" class="song-list-element">
<div class="song-cover-wrapper">
<img
v-if="song.artwork && showArtworks"
:src="song.artwork"
alt="Song cover"
class="song-cover"
>
<i v-else class="fa-solid fa-music song-cover"></i>
</div>
<div class="song-details">
<h3>{{ song.name }}</h3>
<p>{{ song.artist }}</p>
<p>{{ song['additional-info'] }}</p>
</div>
<div class="song-actions">
<p>In {{ timeToPlay( index ) }}min</p>
</div>
</div>
</div>
</div>
+4
View File
@@ -33,5 +33,9 @@
position: fixed;
z-index: 10000;
}
&.disallowed {
cursor: default;
}
}
}
+5
View File
@@ -4,4 +4,9 @@ import type {
declare global {
var MusicKit: MusicKitObject;
interface GlobalEventHandlersEventMap {
'musicplayer:playpause': CustomEvent<void>;
'musicplayer:playindex': CustomEvent<void>;
'musicplayer:seek': CustomEvent<void>;
}
}
+23 -5
View File
@@ -4,6 +4,8 @@ import {
queueIdx
} from '../player/state';
import {
onMounted,
onUnmounted,
ref,
watch
} from 'vue';
@@ -116,7 +118,8 @@ const sendStateData = () => {
request.post( `/room/${ room.value }/update/state`, JSON.stringify( {
'playing': isPlaying.value,
'index': queueIdx.value,
'start': new Date().getTime() - ( playbackPercentage.value * ( queue.value[ queueIdx.value ]?.duration ?? 0 ) ) - 100
'start': new Date().getTime() - 100,
'offset': playbackPercentage.value * ( queue.value[ queueIdx.value ]?.duration ?? 0 )
} ) );
setTimeout( () => {
@@ -128,10 +131,25 @@ const sendStateData = () => {
const useRoomWatchers = () => {
watch( queue, sendPlaylistData );
watch( [
isPlaying,
queueIdx
], sendStateData );
onMounted( () => {
document.addEventListener( 'musicplayer:playindex', sendStateData );
document.addEventListener( 'musicplayer:seek', sendStateData );
document.addEventListener( 'musicplayer:playpause', sendStateData );
} );
onUnmounted( () => {
try {
document.addEventListener( 'musicplayer:play', sendStateData );
} catch { /* empty */ }
try {
document.addEventListener( 'musicplayer:seek', sendStateData );
} catch { /* empty */ }
try {
document.addEventListener( 'musicplayer:playpause', sendStateData );
} catch { /* empty */ }
} );
if ( room.value ) connect();
};
+3 -3
View File
@@ -29,7 +29,6 @@ import type {
import {
load
} from './playlists/loader';
import messages from '../messages';
import {
playIndex
} from './playlists';
@@ -52,6 +51,7 @@ const play = () => {
sources[currentSource.value]?.play();
startTracking();
document.dispatchEvent( new CustomEvent( 'musicplayer:playpause' ) );
};
const pause = () => {
@@ -61,6 +61,7 @@ const pause = () => {
sources[currentSource.value]?.pause();
stopTracking();
document.dispatchEvent( new CustomEvent( 'musicplayer:playpause' ) );
};
/**
@@ -71,6 +72,7 @@ const seekTo = ( pos: number ) => {
if ( currentSource.value === '' ) return;
sources[currentSource.value]?.seekTo( pos );
document.dispatchEvent( new CustomEvent( 'musicplayer:seek' ) );
};
const skip10 = () => {
@@ -153,8 +155,6 @@ const addSongFromSource = async ( source: string, cb?: ( songs: Song[] ) => void
return true;
};
messages.useRoomWatchers();
export default {
play,
pause,
+3 -3
View File
@@ -16,8 +16,8 @@ import type {
Song
} from '@/ts/dtype/playlist';
import {
playlistIdx
} from '@/ts/userPlaylists/state';
setPlaylistIdx
} from '@/ts/userPlaylists';
export const addSongList = ( songs: Song[] ) => {
rawQueue.value = rawQueue.value.concat( songs );
@@ -25,7 +25,7 @@ export const addSongList = ( songs: Song[] ) => {
};
export const clearQueue = () => {
playlistIdx.value = -1;
setPlaylistIdx( 0 );
queue.value = [];
rawQueue.value = [];
sources[currentSource.value]?.stop();
+4 -1
View File
@@ -15,7 +15,7 @@ import {
* 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 ) => {
export const playIndex = ( idx: number ) => {
if ( idx >= queue.value.length ) {
idx = repeat.value === 'all' ? 0 : -1;
} else if ( idx < 0 ) {
@@ -42,4 +42,7 @@ export const playIndex = async ( idx: number ) => {
startTracking();
isPlaying.value = true;
setTimeout( () => {
document.dispatchEvent( new CustomEvent( 'musicplayer:playindex', {} ) );
}, 500 );
};
+4
View File
@@ -6,6 +6,8 @@ import {
currentQueue,
currentQueueIdx,
isPlaying,
playbackOffset,
playbackTime,
startTime
} from './state';
import type {
@@ -40,6 +42,8 @@ const connect = (): Promise<void> => {
currentQueueIdx.value = data.state.index;
isPlaying.value = data.state.playing;
startTime.value = data.state.start;
playbackTime.value = data.state.offset;
playbackOffset.value = data.state.offset;
resolve();
};
+5
View File
@@ -2,6 +2,8 @@ import {
currentQueue,
currentQueueIdx,
isPlaying,
playbackOffset,
playbackTime,
startTime
} from './state';
import type {
@@ -17,6 +19,7 @@ export interface StateUpdate {
'playing': boolean;
'index': number;
'start': number;
'offset': number;
}
export const messageHandler = ( msg: string ) => {
@@ -36,6 +39,8 @@ export const messageHandler = ( msg: string ) => {
currentQueueIdx.value = state.index;
isPlaying.value = state.playing;
startTime.value = state.start;
playbackTime.value = state.offset;
playbackOffset.value = state.offset;
} else {
console.log( '[SSE] Received unknown data', data.type );
}
+6
View File
@@ -13,3 +13,9 @@ export const isPlaying = ref( false );
export const currentQueueIdx = ref( -1 );
export const startTime = ref( new Date().getTime() );
export const showArtworks = ref( false );
export const playbackTime = ref( 0 );
export const playbackOffset = ref( 0 );
+13
View File
@@ -1,7 +1,10 @@
import {
editingPlaylists,
playlistIdx,
playlists
} from './state';
import player from '../player';
export const addPlaylist = ( name: string ) => {
playlists.value.push( {
@@ -17,3 +20,13 @@ export const removePlaylist = ( idx: number ) => {
editingPlaylists.value.splice( idx, 1 );
}
};
export const selectPlaylist = ( idx: number ) => {
playlistIdx.value = idx;
player.clearQueue();
player.loadPlaylist( playlists.value[ idx ]!.songs );
};
export const setPlaylistIdx = ( idx: number ) => {
playlistIdx.value = idx;
};
+4 -2
View File
@@ -16,15 +16,17 @@ import {
useNotification
} from '@kyvg/vue3-notification';
export const savePlaylist = async () => {
export const savePlaylist = () => {
rawQueue.value = queue.value;
shuffle.value = false;
if ( playlistIdx.value ) {
if ( playlistIdx.value < 0 ) {
let name: null | string = '';
while ( !name || name.length === 0 ) {
name = prompt( 'You are trying to save a playlist that has not previously been created. Please enter a name for it' );
if ( !name ) return;
}
addPlaylist( name );
+39 -2
View File
@@ -1,21 +1,49 @@
<script setup lang="ts">
import {
type ComputedRef,
computed
computed,
onMounted,
ref
} from 'vue';
import {
currentQueue,
currentQueueIdx
currentQueueIdx,
isPlaying,
playbackOffset,
playbackTime,
startTime
} from '@/ts/shared/state';
import CurrentSong from '@/components/player/CurrentSong.vue';
import ProgressBar from '@/components/player/ProgressBar.vue';
import SharedQueue from '@/components/shared/SharedQueue.vue';
import type {
Song
} from '@/ts/dtype/playlist';
import {
beautifyTime
} from '@/ts/util/time';
import shared from '@/ts/shared';
shared.connect();
const playbackProgress = ref( 0 );
onMounted( () => {
setInterval( () => {
if ( !isPlaying.value ) return;
playbackTime.value = ( ( new Date().getTime() - startTime.value ) / 1000 ) + playbackOffset.value;
playbackProgress.value = playbackTime.value / song.value.duration;
if ( playbackTime.value > song.value.duration ) {
playbackOffset.value -= song.value.duration;
if ( currentQueueIdx.value < currentQueue.value.length - 1 )
currentQueueIdx.value++;
else
isPlaying.value = false;
}
}, 250 );
} );
const song: ComputedRef<Song> = computed( () => {
if ( currentQueueIdx.value >= 0 )
return currentQueue.value[currentQueueIdx.value]!;
@@ -37,6 +65,15 @@
<div>
<div>
<CurrentSong v-model="song" />
<div class="time">
<p class="current">
{{ beautifyTime( playbackTime ) }}
</p>
<p class="duration">
{{ beautifyTime( song.duration ) }}
</p>
</div>
<ProgressBar v-model="playbackProgress" :disallow-move="true" />
</div>
<div>
<SharedQueue />