fix(media): publish status when a track ends naturally
The Redis media:status key was only rewritten after a command received via Redis. When the last track ended naturally (AudioPlayer Idle -> advanceQueue with an empty queue), currentTrackItem was cleared but the status key was not persisted — so the backend's cached status and the frontend's 10s polling stayed stuck showing the finished track as 'playing' forever. Wire a media-status sink (commandHandler provides the real redisPub to MediaHandler) and re-publish status after auto-advance, so natural track end updates the UI.
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
|||||||
createHandlerRegistry,
|
createHandlerRegistry,
|
||||||
} from "./handler-registry.js";
|
} from "./handler-registry.js";
|
||||||
import { MediaHandler } from "./media.handler.js";
|
import { MediaHandler } from "./media.handler.js";
|
||||||
|
import { wireMediaStatusWriter } from "./mediaStatusSink.js";
|
||||||
import { ModerationHandler } from "./moderation.handler.js";
|
import { ModerationHandler } from "./moderation.handler.js";
|
||||||
import { VoiceHandler } from "./voice.handler.js";
|
import { VoiceHandler } from "./voice.handler.js";
|
||||||
|
|
||||||
@@ -88,6 +89,10 @@ export class CommandHandler {
|
|||||||
this.guildHandler = new GuildHandler(client);
|
this.guildHandler = new GuildHandler(client);
|
||||||
this.moderationHandler = new ModerationHandler(client);
|
this.moderationHandler = new ModerationHandler(client);
|
||||||
|
|
||||||
|
// Wire the media status sink so MediaHandler can persist status on
|
||||||
|
// queue advances that happen outside a command (natural track end).
|
||||||
|
wireMediaStatusWriter(this.redisPub);
|
||||||
|
|
||||||
// Build the command registry
|
// Build the command registry
|
||||||
this.registry = createHandlerRegistry(
|
this.registry = createHandlerRegistry(
|
||||||
this.voiceHandler,
|
this.voiceHandler,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
ScreenShareController,
|
ScreenShareController,
|
||||||
type ScreenShareVoiceStatus,
|
type ScreenShareVoiceStatus,
|
||||||
} from "../voice-recording/screenShareController.js";
|
} from "../voice-recording/screenShareController.js";
|
||||||
|
import { setMediaStatusKey } from "./mediaStatusSink.js";
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Types
|
// Types
|
||||||
@@ -88,14 +89,36 @@ export class MediaHandler {
|
|||||||
activeChannelId: null,
|
activeChannelId: null,
|
||||||
}),
|
}),
|
||||||
) {
|
) {
|
||||||
// Register auto-advance on natural track end
|
// Register auto-advance on natural track end. advanceQueue mutates the
|
||||||
|
// module-level currentTrackItem/queue, so we must re-publish the status
|
||||||
|
// key afterward: otherwise the backend's Redis `media:status` cache (and
|
||||||
|
// the frontend's 10s polling) stays stuck on the finished track.
|
||||||
discordPlayer.onIdle(() => {
|
discordPlayer.onIdle(() => {
|
||||||
this.advanceQueue().catch((err) => {
|
this.advanceQueue()
|
||||||
this.logger.error({ err }, "Auto-advance failed");
|
.then(() => this.publishStatus())
|
||||||
});
|
.catch((err) => {
|
||||||
|
this.logger.error({ err }, "Auto-advance failed");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persist the latest media state to Redis so the backend/frontend see queue
|
||||||
|
* advances that happen outside a command (natural track end, screen-share
|
||||||
|
* done). CommandHandler owns the Redis status-key writes for command-triggered
|
||||||
|
* changes; this covers the side-effect-only path.
|
||||||
|
*/
|
||||||
|
private publishStatus(): void {
|
||||||
|
try {
|
||||||
|
setMediaStatusKey(this.getCurrentMediaStatus());
|
||||||
|
} catch (err: unknown) {
|
||||||
|
this.logger.warn(
|
||||||
|
{ error: err instanceof Error ? err.message : String(err) },
|
||||||
|
"Failed to publish media status on track end",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getCurrentMediaStatus(): MediaStatusPayload {
|
getCurrentMediaStatus(): MediaStatusPayload {
|
||||||
return buildStatusPayload();
|
return buildStatusPayload();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import type Redis from "ioredis";
|
||||||
|
import { createChildLogger } from "@/shared/logger/index";
|
||||||
|
import { MEDIA_STATUS_KEY } from "../../shared/redis-channels.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared sink for writing the media status Redis key.
|
||||||
|
*
|
||||||
|
* CommandHandler owns the publisher + status writes for command-triggered
|
||||||
|
* changes (`publishMediaStatus`). MediaHandler needs to also persist status
|
||||||
|
* when the queue advances *outside* a command (natural track end / screen-share
|
||||||
|
* done), so we expose the real publisher here and let CommandHandler wire it
|
||||||
|
* once at startup.
|
||||||
|
*/
|
||||||
|
const logger = createChildLogger("media-status-sink");
|
||||||
|
|
||||||
|
let _setMediaStatusKey: ((payload: unknown) => void) | null = null;
|
||||||
|
|
||||||
|
export function setMediaStatusWriter(writer: (payload: unknown) => void): void {
|
||||||
|
_setMediaStatusKey = writer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setMediaStatusKey(payload: unknown): void {
|
||||||
|
if (!_setMediaStatusKey) {
|
||||||
|
logger.warn("Media status writer not wired — skipping status publish");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_setMediaStatusKey(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { MEDIA_STATUS_KEY };
|
||||||
|
|
||||||
|
export function wireMediaStatusWriter(redisPub: Redis): void {
|
||||||
|
setMediaStatusWriter((payload) => {
|
||||||
|
redisPub
|
||||||
|
.set(MEDIA_STATUS_KEY, JSON.stringify(payload))
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
|
logger.warn({ error: msg }, "Failed to update media status key");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user