refactor(media): remove volume control from FE & BE
Volume sudah di-set default 0.3 di gateway (suara kecil saat play), dan user bisa naikin sendiri di Discord (command media:volume) — jadi kontrol volume lewat dashboard tak perlu. Hapus: - BE: POST /api/media/volume route, mediaVolumeSchema, setVolume service - FE: useMediaVolume hook, mediaApi.volume, slider volume di music-player + mini-player, field volume/setVolume di MediaPlayerProvider Pertahankan COMMAND_MEDIA_VOLUME di gateway (masih dipakai command DC) dan mic volume (terpisah, tetap di voice page).
This commit is contained in:
@@ -2,8 +2,8 @@ import type { Request, Response, Router } from "express";
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import { createChildLogger } from "@/shared/logger/index";
|
import { createChildLogger } from "@/shared/logger/index";
|
||||||
import { asyncHandler, validateBody } from "../../shared/middlewares/index.js";
|
import { asyncHandler, validateBody } from "../../shared/middlewares/index.js";
|
||||||
import { mediaQueueSchema, mediaVolumeSchema } from "./media.schema.js";
|
import { mediaQueueSchema } from "./media.schema.js";
|
||||||
import { getStatus, queue, setVolume, skip, stop } from "./media.service.js";
|
import { getStatus, queue, skip, stop } from "./media.service.js";
|
||||||
|
|
||||||
const logger = createChildLogger("media.routes");
|
const logger = createChildLogger("media.routes");
|
||||||
|
|
||||||
@@ -55,17 +55,5 @@ export function createMediaRouter(): Router {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// POST /api/media/volume
|
|
||||||
router.post(
|
|
||||||
"/media/volume",
|
|
||||||
validateBody(mediaVolumeSchema),
|
|
||||||
asyncHandler(async (req: Request, res: Response) => {
|
|
||||||
const { volume } = req.body as { volume: number };
|
|
||||||
logger.debug({ volume }, "Media volume requested");
|
|
||||||
const state = await setVolume(volume);
|
|
||||||
res.json(state);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,4 @@ export const mediaQueueSchema = z.object({
|
|||||||
mode: z.enum(["music", "screen"]).default("music"),
|
mode: z.enum(["music", "screen"]).default("music"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const mediaVolumeSchema = z.object({
|
|
||||||
volume: z.number().min(0).max(1).default(0.3),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type MediaQueueInput = z.infer<typeof mediaQueueSchema>;
|
export type MediaQueueInput = z.infer<typeof mediaQueueSchema>;
|
||||||
export type MediaVolumeInput = z.infer<typeof mediaVolumeSchema>;
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
COMMAND_MEDIA_QUEUE,
|
COMMAND_MEDIA_QUEUE,
|
||||||
COMMAND_MEDIA_SKIP,
|
COMMAND_MEDIA_SKIP,
|
||||||
COMMAND_MEDIA_STOP,
|
COMMAND_MEDIA_STOP,
|
||||||
COMMAND_MEDIA_VOLUME,
|
|
||||||
MEDIA_STATUS_KEY,
|
MEDIA_STATUS_KEY,
|
||||||
} from "../../shared/index.js";
|
} from "../../shared/index.js";
|
||||||
import { publishCommand, readRedisStatus } from "../../shared/redis/index.js";
|
import { publishCommand, readRedisStatus } from "../../shared/redis/index.js";
|
||||||
@@ -149,20 +148,3 @@ export async function stop(): Promise<MediaState> {
|
|||||||
"stop",
|
"stop",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Set volume via Redis command to discord-gateway.
|
|
||||||
*/
|
|
||||||
export async function setVolume(volume: number): Promise<MediaState> {
|
|
||||||
logger.info({ volume }, "setVolume called");
|
|
||||||
return tryCommandThenFallback(
|
|
||||||
() =>
|
|
||||||
publishCommand<MediaState>(
|
|
||||||
COMMAND_MEDIA_VOLUME,
|
|
||||||
{ volume },
|
|
||||||
DEFAULT_COMMAND_TIMEOUT_MS,
|
|
||||||
),
|
|
||||||
() => readStatusFallback(),
|
|
||||||
"setVolume",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Disc3, Music, SkipForward, Square, Volume2 } from "lucide-react";
|
import { Disc3, Music, SkipForward, Square } from "lucide-react";
|
||||||
import { useMediaPlayer } from "@/lib/hooks/use-media-player";
|
import { useMediaPlayer } from "@/lib/hooks/use-media-player";
|
||||||
|
|
||||||
export function MiniPlayer() {
|
export function MiniPlayer() {
|
||||||
const { playing, current, queue, volume, pending, skip, stop, setVolume } =
|
const { playing, current, queue, pending, skip, stop } = useMediaPlayer();
|
||||||
useMediaPlayer();
|
|
||||||
|
|
||||||
// Nothing to show if no track is playing and nothing is queued
|
// Nothing to show if no track is playing and nothing is queued
|
||||||
if (!current && queue.length === 0) return null;
|
if (!current && queue.length === 0) return null;
|
||||||
@@ -61,22 +60,6 @@ export function MiniPlayer() {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Volume */}
|
|
||||||
<div className="flex items-center gap-2 shrink-0 ml-2">
|
|
||||||
<Volume2 className="size-3.5 text-text-secondary/60" />
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="1"
|
|
||||||
step="0.05"
|
|
||||||
value={volume}
|
|
||||||
onChange={(e) => setVolume(Number(e.target.value))}
|
|
||||||
className="w-20 h-1 appearance-none rounded-full bg-glass-bg accent-primary cursor-pointer
|
|
||||||
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:size-3 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-primary"
|
|
||||||
aria-label="Volume"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,21 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Disc3, Music, Play, SkipForward, Square, Volume2 } from "lucide-react";
|
import { Disc3, Music, Play, SkipForward, Square } from "lucide-react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Slider } from "@/components/ui/slider";
|
|
||||||
import {
|
import {
|
||||||
useMediaQueue,
|
useMediaQueue,
|
||||||
useMediaSkip,
|
useMediaSkip,
|
||||||
useMediaState,
|
useMediaState,
|
||||||
useMediaStop,
|
useMediaStop,
|
||||||
useMediaVolume,
|
|
||||||
useMediaWsSync,
|
useMediaWsSync,
|
||||||
} from "@/hooks";
|
} from "@/hooks";
|
||||||
import type { WsHook } from "@/lib/ws-hook";
|
|
||||||
|
|
||||||
import type { MediaState } from "@/lib/types";
|
import type { MediaState } from "@/lib/types";
|
||||||
|
import type { WsHook } from "@/lib/ws-hook";
|
||||||
|
|
||||||
interface MusicPlayerProps {
|
interface MusicPlayerProps {
|
||||||
ws: WsHook;
|
ws: WsHook;
|
||||||
@@ -31,7 +28,6 @@ export function MusicPlayer({ ws, initialData }: MusicPlayerProps) {
|
|||||||
const queueMut = useMediaQueue();
|
const queueMut = useMediaQueue();
|
||||||
const skipMut = useMediaSkip();
|
const skipMut = useMediaSkip();
|
||||||
const stopMut = useMediaStop();
|
const stopMut = useMediaStop();
|
||||||
const volumeMut = useMediaVolume();
|
|
||||||
const [queueUrl, setQueueUrl] = useState("");
|
const [queueUrl, setQueueUrl] = useState("");
|
||||||
const [screenMode, setScreenMode] = useState(false);
|
const [screenMode, setScreenMode] = useState(false);
|
||||||
|
|
||||||
@@ -47,14 +43,6 @@ export function MusicPlayer({ ws, initialData }: MusicPlayerProps) {
|
|||||||
setQueueUrl("");
|
setQueueUrl("");
|
||||||
}, [queueUrl, queueMut, screenMode]);
|
}, [queueUrl, queueMut, screenMode]);
|
||||||
|
|
||||||
const handleVolume = useCallback(
|
|
||||||
(value: number | readonly number[]) => {
|
|
||||||
const vol = Array.isArray(value) ? value[0] : value;
|
|
||||||
volumeMut.mutate(vol);
|
|
||||||
},
|
|
||||||
[volumeMut],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -143,18 +131,6 @@ export function MusicPlayer({ ws, initialData }: MusicPlayerProps) {
|
|||||||
<SkipForward className="size-4 mr-1" />
|
<SkipForward className="size-4 mr-1" />
|
||||||
Skip
|
Skip
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex items-center gap-2 ml-auto">
|
|
||||||
<Volume2 className="size-4 text-muted-foreground" />
|
|
||||||
<Slider
|
|
||||||
className="w-24"
|
|
||||||
defaultValue={[mediaState?.musicVolume ?? 0.3]}
|
|
||||||
value={[mediaState?.musicVolume ?? 0.3]}
|
|
||||||
onValueChange={handleVolume}
|
|
||||||
min={0}
|
|
||||||
max={1}
|
|
||||||
step={0.05}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{mediaState && mediaState.queue.length > 0 && (
|
{mediaState && mediaState.queue.length > 0 && (
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export {
|
|||||||
useMediaSkip,
|
useMediaSkip,
|
||||||
useMediaState,
|
useMediaState,
|
||||||
useMediaStop,
|
useMediaStop,
|
||||||
useMediaVolume,
|
|
||||||
useMediaWsSync,
|
useMediaWsSync,
|
||||||
} from "./use-media";
|
} from "./use-media";
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -38,10 +38,6 @@ export function useMediaStop() {
|
|||||||
return useMediaAction(() => mediaApi.stop());
|
return useMediaAction(() => mediaApi.stop());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMediaVolume() {
|
|
||||||
return useMediaAction((volume: number) => mediaApi.volume(volume));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Subscribe to WS media_state events to keep cache fresh */
|
/** Subscribe to WS media_state events to keep cache fresh */
|
||||||
export function useMediaWsSync(ws: WsHook) {
|
export function useMediaWsSync(ws: WsHook) {
|
||||||
const { mutate } = useSWRConfig();
|
const { mutate } = useSWRConfig();
|
||||||
|
|||||||
@@ -7,6 +7,4 @@ export const mediaApi = {
|
|||||||
api.post<MediaState>("/api/media/queue", { source, mode }),
|
api.post<MediaState>("/api/media/queue", { source, mode }),
|
||||||
skip: () => api.post<MediaState>("/api/media/skip", {}),
|
skip: () => api.post<MediaState>("/api/media/skip", {}),
|
||||||
stop: () => api.post<MediaState>("/api/media/stop", {}),
|
stop: () => api.post<MediaState>("/api/media/stop", {}),
|
||||||
volume: (volume: number) =>
|
|
||||||
api.post<MediaState>("/api/media/volume", { volume }),
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ interface MediaPlayerContextValue {
|
|||||||
current: MediaItem | null;
|
current: MediaItem | null;
|
||||||
/** Upcoming queue */
|
/** Upcoming queue */
|
||||||
queue: MediaItem[];
|
queue: MediaItem[];
|
||||||
/** Current volume [0-1] */
|
|
||||||
volume: number;
|
|
||||||
/** True while a mutation is in flight */
|
/** True while a mutation is in flight */
|
||||||
pending: boolean;
|
pending: boolean;
|
||||||
|
|
||||||
@@ -29,8 +27,6 @@ interface MediaPlayerContextValue {
|
|||||||
skip: () => void;
|
skip: () => void;
|
||||||
/** Stop playback */
|
/** Stop playback */
|
||||||
stop: () => void;
|
stop: () => void;
|
||||||
/** Set volume [0-1] */
|
|
||||||
setVolume: (vol: number) => void;
|
|
||||||
/** Queue a URL for playback */
|
/** Queue a URL for playback */
|
||||||
queueUrl: (url: string) => void;
|
queueUrl: (url: string) => void;
|
||||||
}
|
}
|
||||||
@@ -96,17 +92,6 @@ export function MediaPlayerProvider({ children }: { children: ReactNode }) {
|
|||||||
.finally(() => setPending(false));
|
.finally(() => setPending(false));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const setVolume = useCallback((vol: number) => {
|
|
||||||
mediaApi
|
|
||||||
.volume(vol)
|
|
||||||
.then((data) => {
|
|
||||||
if (data) setState(data as MediaState);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// ignore
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const queueUrl = useCallback((url: string) => {
|
const queueUrl = useCallback((url: string) => {
|
||||||
setPending(true);
|
setPending(true);
|
||||||
mediaApi
|
mediaApi
|
||||||
@@ -126,11 +111,9 @@ export function MediaPlayerProvider({ children }: { children: ReactNode }) {
|
|||||||
playing: state.playing,
|
playing: state.playing,
|
||||||
current: state.current,
|
current: state.current,
|
||||||
queue: state.queue,
|
queue: state.queue,
|
||||||
volume: state.musicVolume,
|
|
||||||
pending,
|
pending,
|
||||||
skip,
|
skip,
|
||||||
stop,
|
stop,
|
||||||
setVolume,
|
|
||||||
queueUrl,
|
queueUrl,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user