From 1da362da0ba4586968242545c860360ec3253005 Mon Sep 17 00:00:00 2001 From: Lina Date: Fri, 24 Apr 2026 10:54:29 -0700 Subject: [PATCH] Fix removing currently playing song from catalog When the currently playing song is removed from the catalog, the canvas media now stops immediately and skips to the next available song in the queue. If no songs remain, media is set to none. Previously, removing a playing song only updated the data object without stopping the canvas media, causing the removed song to continue playing, loop, or create desync between UI and audio. Co-Authored-By: Claude Opus 4.6 (1M context) --- server/controllers/media/RemoveMedia.ts | 74 ++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/server/controllers/media/RemoveMedia.ts b/server/controllers/media/RemoveMedia.ts index 8dd3cdd..4ee213f 100644 --- a/server/controllers/media/RemoveMedia.ts +++ b/server/controllers/media/RemoveMedia.ts @@ -1,7 +1,9 @@ import redisObj from "../../redis-sse/index.js"; import { Video } from "../../types/index.js"; -import { getCredentials, getDroppedAsset } from "../../utils/index.js"; +import { getCredentials, getDroppedAsset, World } from "../../utils/index.js"; import { Request, Response } from "express"; +import { DroppedAssetMediaType } from "@rtsdk/topia"; +import { getAvailableVideos } from "../../utils/youtube/index.js"; export default async function RemoveMedia(req: Request, res: Response) { const credentials = getCredentials(req.query); @@ -21,6 +23,7 @@ export default async function RemoveMedia(req: Request, res: Response) { const jukeboxUpdate: { catalog?: Video[]; queue?: string[]; + nowPlaying?: string; } = {}; if (type === "catalog") { @@ -32,6 +35,65 @@ export default async function RemoveMedia(req: Request, res: Response) { jukeboxUpdate.queue = jukeboxAsset.dataObject.queue.filter((videoId: string) => !videoIds.includes(videoId)); } + // If the currently playing song was removed, stop it and skip to next + const nowPlayingRemoved = videoIds.includes(jukeboxAsset.dataObject.nowPlaying); + if (nowPlayingRemoved) { + const remainingQueue = jukeboxUpdate.queue ?? jukeboxAsset.dataObject.queue; + const remainingCatalog = jukeboxUpdate.catalog ?? jukeboxAsset.dataObject.catalog; + + // Find next available song from the remaining queue and catalog + let nextVideo: Video | null = null; + let nextIndex = -1; + + if (remainingQueue.length > 0 && remainingCatalog.length > 0) { + const availableVideoIds = await getAvailableVideos(remainingCatalog); + for (let i = 0; i < remainingQueue.length; i++) { + const video = remainingCatalog.find((v: Video) => v.id.videoId === remainingQueue[i]); + if (video && availableVideoIds.includes(video.id.videoId)) { + nextVideo = video; + nextIndex = i; + break; + } + } + } + + if (nextVideo) { + // Play the next song + const mediaLink = `https://www.youtube.com/watch?v=${nextVideo.id.videoId}`; + await jukeboxAsset.updateMediaType({ + mediaLink, + isVideo: + (jukeboxAsset.dataObject.settings?.mode ?? (process.env.AUDIO_ONLY ? "jukebox" : "karaoke")) === "karaoke", + mediaName: "Jukebox", + mediaType: DroppedAssetMediaType.LINK, + audioSliderVolume: (jukeboxAsset as any).audioSliderVolume || 10, + audioRadius: (jukeboxAsset as any).audioRadius || 2, + portalName: "", + syncUserMedia: true, + }); + + jukeboxUpdate.nowPlaying = nextVideo.id.videoId; + jukeboxUpdate.queue = remainingQueue.slice(nextIndex + 1); + + const world = World.create(urlSlug, { credentials }); + world + .triggerParticle({ + name: "musicNote_float", + duration: 10, + position: { + x: jukeboxAsset.position.x, + y: jukeboxAsset.position.y - 130, + }, + }) + .catch(() => console.error("Cannot trigger particle")); + } else { + // No next song — stop media + await jukeboxAsset.updateMediaType({ mediaType: DroppedAssetMediaType.NONE } as any); + jukeboxUpdate.nowPlaying = "-1"; + jukeboxUpdate.queue = []; + } + } + await jukeboxAsset.updateDataObject( { ...jukeboxAsset.dataObject, @@ -45,6 +107,16 @@ export default async function RemoveMedia(req: Request, res: Response) { }, ); + // Publish appropriate SSE event + if (nowPlayingRemoved) { + redisObj.publish(`${process.env.INTERACTIVE_KEY}_JUKEBOX`, { + assetId: jukeboxAsset.id, + videoId: jukeboxUpdate.nowPlaying !== "-1" ? jukeboxUpdate.nowPlaying : "-1", + nextUpId: jukeboxUpdate.queue && jukeboxUpdate.queue.length > 0 ? jukeboxUpdate.queue[0] : null, + event: "nowPlaying", + }); + } + redisObj.publish(`${process.env.INTERACTIVE_KEY}_JUKEBOX`, { assetId: jukeboxAsset.id, videos: videoIds,