Skip to content

Commit f4934d0

Browse files
committed
fix: Resolve listener playback initialization loop │
│ │ │ - Add duplicate command prevention for loadVideoById to stop repeated queueing │ │ - Update player state when loading videos to track current video properly │ │ - Fix heartbeat sync logic to only sync when video actually changes │ │ - Skip sync operations while player is still initializing │ │ - Add 3-second timeout for duplicate load command detection │ │ - Ensure video state comparison only happens when heartbeat contains video ID │ │ │ │ This fixes the issue where listeners would get stuck in endless loadVideoById │ │ loops preventing them from joining playback properly.
1 parent 95546df commit f4934d0

10 files changed

Lines changed: 554 additions & 58 deletions

File tree

src/main.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,9 @@ Hooks.once('ready', async () => {
193193
const savedQueuesManager = new SavedQueuesManager(store, queueManager);
194194
logger.info('YouTube DJ SavedQueuesManager initialized globally');
195195

196+
// Connect QueueManager to SavedQueuesManager for queue modification tracking
197+
queueManager.setSavedQueuesManager(savedQueuesManager);
198+
196199
// Store global references for access across components
197200
(globalThis as any).youtubeDJSocketManager = socketManager;
198201
(globalThis as any).youtubeDJSessionManager = sessionManager;

src/services/PlayerManager.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -775,11 +775,18 @@ export class PlayerManager {
775775
return; // DJ doesn't sync to their own heartbeat
776776
}
777777

778+
// Skip sync if player is still initializing
779+
const playerState = this.store.getPlayerState();
780+
if (playerState.isInitializing) {
781+
logger.debug('🎵 YouTube DJ | Skipping heartbeat sync - player still initializing');
782+
return;
783+
}
784+
778785
// Only sync if we have a different video or significant time difference
779-
const currentVideo = this.store.getPlayerState().currentVideo;
786+
const currentVideo = playerState.currentVideo;
780787

781-
// Check if video changed
782-
if (currentVideo?.videoId !== heartbeat.videoId) {
788+
// Check if video changed (only if heartbeat has a video)
789+
if (heartbeat.videoId && currentVideo?.videoId !== heartbeat.videoId) {
783790
logger.debug('🎵 YouTube DJ | Syncing to new video from heartbeat:', heartbeat.videoId);
784791
// Send load command to widget
785792
Hooks.callAll('youtubeDJ.playerCommand', {

src/services/QueueManager.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export interface YouTubeDJMessage {
1616

1717
export class QueueManager {
1818
private store: SessionStore;
19+
private savedQueuesManager?: any; // Will be set after initialization
1920

2021
constructor(store: SessionStore) {
2122
this.store = store;
@@ -36,6 +37,22 @@ export class QueueManager {
3637
Hooks.on('youtubeDJ.playlistEmbedError', this.onPlaylistEmbedError.bind(this));
3738
}
3839

40+
/**
41+
* Set reference to SavedQueuesManager (called after initialization)
42+
*/
43+
setSavedQueuesManager(savedQueuesManager: any): void {
44+
this.savedQueuesManager = savedQueuesManager;
45+
}
46+
47+
/**
48+
* Mark queue as modified if there's a currently loaded saved queue
49+
*/
50+
private markQueueAsModified(): void {
51+
if (this.savedQueuesManager) {
52+
this.savedQueuesManager.markQueueAsModified();
53+
}
54+
}
55+
3956
/**
4057
* Check if user can add videos to queue
4158
*/
@@ -215,6 +232,9 @@ export class QueueManager {
215232
}
216233
});
217234

235+
// Mark queue as modified if there's a loaded saved queue
236+
this.markQueueAsModified();
237+
218238
// Broadcast queue update
219239
this.broadcastMessage({
220240
type: 'QUEUE_ADD',
@@ -279,6 +299,9 @@ export class QueueManager {
279299
}
280300
});
281301

302+
// Mark queue as modified if there's a loaded saved queue
303+
this.markQueueAsModified();
304+
282305
// Broadcast queue update
283306
this.broadcastMessage({
284307
type: 'QUEUE_REMOVE',
@@ -338,6 +361,9 @@ export class QueueManager {
338361
}
339362
});
340363

364+
// Mark queue as modified if there's a loaded saved queue
365+
this.markQueueAsModified();
366+
341367
// Broadcast queue update
342368
this.broadcastMessage({
343369
type: 'QUEUE_UPDATE',
@@ -611,7 +637,10 @@ export class QueueManager {
611637
items: [],
612638
currentIndex: -1,
613639
mode: 'single-dj',
614-
djUserId: this.store.getSessionState().djUserId
640+
djUserId: this.store.getSessionState().djUserId,
641+
savedQueues: this.store.getQueueState().savedQueues,
642+
currentlyLoadedQueueId: null, // Clear loaded queue tracking
643+
isModifiedFromSaved: false
615644
},
616645
player: {
617646
...this.store.getPlayerState(),

src/services/SavedQueuesManager.ts

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,15 @@ export class SavedQueuesManager {
115115
// Save to world settings
116116
await game.settings.set('core', 'youtubeDJ.savedQueues', updatedQueues);
117117

118+
// Update queue state to track this as the currently loaded queue
119+
this.store.updateState({
120+
queue: {
121+
...currentQueue,
122+
currentlyLoadedQueueId: savedQueue.id,
123+
isModifiedFromSaved: false
124+
}
125+
});
126+
118127
// Broadcast save event
119128
this.broadcastMessage({
120129
type: 'QUEUE_SAVED',
@@ -160,12 +169,15 @@ export class SavedQueuesManager {
160169
newItems = [...currentQueue.items, ...savedQueue.items];
161170
}
162171

163-
// Update queue state
172+
// Update queue state with tracking
164173
this.store.updateState({
165174
queue: {
166175
...currentQueue,
167176
items: newItems,
168-
currentIndex: replace && newItems.length > 0 ? 0 : currentQueue.currentIndex
177+
currentIndex: replace && newItems.length > 0 ? 0 : currentQueue.currentIndex,
178+
// Track the loaded queue only if we replaced (not if we appended)
179+
currentlyLoadedQueueId: replace ? queueId : null,
180+
isModifiedFromSaved: false
169181
}
170182
});
171183

@@ -324,6 +336,134 @@ export class SavedQueuesManager {
324336
ui.notifications?.success(`Queue renamed to "${trimmedName}"`);
325337
}
326338

339+
/**
340+
* Save changes to the currently loaded saved queue
341+
*/
342+
async saveChangesToCurrentQueue(): Promise<SavedQueue | null> {
343+
if (!this.store.isDJ()) {
344+
throw new Error('Only the DJ can save queue changes');
345+
}
346+
347+
const currentQueue = this.store.getQueueState();
348+
349+
// Check if there's a currently loaded saved queue
350+
if (!currentQueue.currentlyLoadedQueueId) {
351+
throw new Error('No saved queue is currently loaded. Use "Save Queue" to create a new saved queue.');
352+
}
353+
354+
// Check if the queue has been modified
355+
if (!currentQueue.isModifiedFromSaved) {
356+
ui.notifications?.info('No changes to save - queue is already up to date');
357+
return null;
358+
}
359+
360+
const savedQueue = this.getSavedQueue(currentQueue.currentlyLoadedQueueId);
361+
if (!savedQueue) {
362+
throw new Error('Currently loaded saved queue not found');
363+
}
364+
365+
if (currentQueue.items.length === 0) {
366+
throw new Error('Cannot save an empty queue');
367+
}
368+
369+
logger.debug('🎵 YouTube DJ | Saving changes to currently loaded queue:', savedQueue.name);
370+
371+
// Create updated saved queue
372+
const updatedQueue: SavedQueue = {
373+
...savedQueue,
374+
items: [...currentQueue.items], // Deep copy current items
375+
updatedAt: Date.now()
376+
};
377+
378+
// Update saved queues list
379+
const savedQueues = this.getSavedQueues();
380+
const updatedQueues = savedQueues.map(q =>
381+
q.id === currentQueue.currentlyLoadedQueueId ? updatedQueue : q
382+
);
383+
384+
// Save to world settings
385+
await game.settings.set('core', 'youtubeDJ.savedQueues', updatedQueues);
386+
387+
// Update queue state to reflect saved changes
388+
this.store.updateState({
389+
queue: {
390+
...currentQueue,
391+
isModifiedFromSaved: false
392+
}
393+
});
394+
395+
// Broadcast save event
396+
this.broadcastMessage({
397+
type: 'QUEUE_SAVED',
398+
userId: game.user?.id || '',
399+
timestamp: Date.now(),
400+
data: {
401+
savedQueue: updatedQueue,
402+
isOverwrite: true,
403+
isCurrentQueueUpdate: true
404+
}
405+
});
406+
407+
logger.info('🎵 YouTube DJ | Changes saved to queue:', savedQueue.name);
408+
ui.notifications?.success(`Changes saved to "${savedQueue.name}"`);
409+
410+
return updatedQueue;
411+
}
412+
413+
/**
414+
* Get information about the currently loaded saved queue
415+
*/
416+
getCurrentlyLoadedQueue(): { savedQueue: SavedQueue | null; hasChanges: boolean } {
417+
const currentQueue = this.store.getQueueState();
418+
419+
if (!currentQueue.currentlyLoadedQueueId) {
420+
return { savedQueue: null, hasChanges: false };
421+
}
422+
423+
const savedQueue = this.getSavedQueue(currentQueue.currentlyLoadedQueueId);
424+
return {
425+
savedQueue,
426+
hasChanges: currentQueue.isModifiedFromSaved
427+
};
428+
}
429+
430+
/**
431+
* Mark the current queue as modified from the saved version
432+
*/
433+
markQueueAsModified(): void {
434+
const currentQueue = this.store.getQueueState();
435+
436+
if (currentQueue.currentlyLoadedQueueId && !currentQueue.isModifiedFromSaved) {
437+
this.store.updateState({
438+
queue: {
439+
...currentQueue,
440+
isModifiedFromSaved: true
441+
}
442+
});
443+
444+
logger.debug('🎵 YouTube DJ | Queue marked as modified from saved version');
445+
}
446+
}
447+
448+
/**
449+
* Clear the currently loaded queue tracking (when creating a new queue from scratch)
450+
*/
451+
clearCurrentlyLoadedQueue(): void {
452+
const currentQueue = this.store.getQueueState();
453+
454+
if (currentQueue.currentlyLoadedQueueId || currentQueue.isModifiedFromSaved) {
455+
this.store.updateState({
456+
queue: {
457+
...currentQueue,
458+
currentlyLoadedQueueId: null,
459+
isModifiedFromSaved: false
460+
}
461+
});
462+
463+
logger.debug('🎵 YouTube DJ | Cleared currently loaded queue tracking');
464+
}
465+
}
466+
327467
/**
328468
* Export a saved queue to JSON
329469
*/

src/state/StateTypes.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ export interface QueueState {
109109
mode: 'single-dj' | 'collaborative';
110110
djUserId: string | null;
111111
savedQueues: SavedQueue[];
112+
// Track the currently loaded saved queue (if any)
113+
currentlyLoadedQueueId: string | null;
114+
isModifiedFromSaved: boolean; // Track if current queue differs from the loaded saved queue
112115
}
113116

114117
export interface UIState {
@@ -171,7 +174,9 @@ export function createDefaultQueueState(): QueueState {
171174
currentIndex: -1,
172175
mode: 'single-dj',
173176
djUserId: null,
174-
savedQueues: []
177+
savedQueues: [],
178+
currentlyLoadedQueueId: null,
179+
isModifiedFromSaved: false
175180
};
176181
}
177182

0 commit comments

Comments
 (0)