대부분 모든기능 제작

하단 player연결, 일시정지, 스킵, 볼륨, 특정시간재생, queue 디자인 변경, queue 변경기능 제작
This commit is contained in:
tkrmagid-desktop
2026-04-10 00:40:40 +09:00
parent 88a5a88c51
commit 374fbdb1ce
18 changed files with 1265 additions and 83 deletions

View File

@@ -1,6 +1,6 @@
import { Guild, Message, TextChannel } from "discord.js";
import { LoadType, Player, Track, TrackEndEvent } from "shoukaku";
import { client, lavalinkManager } from "../index";
import { LoadType, Player, Track, TrackEndEvent, TrackStartEvent } from "shoukaku";
import { client, lavalinkManager, Redis } from "../index";
import { timeFormat } from "../utils/music/Utils";
import { default_content, default_embed, default_image, getButtons } from "../utils/music/Config";
import { GuildType } from "../../db/db";
@@ -30,6 +30,11 @@ export class GuildPlayer {
public msg: Message,
) {
this.player.setGlobalVolume(50);
this.player.on("start", (_data: TrackStartEvent) => {
Redis.publishState("player_update", {
guildId: this.guild.id,
});
});
this.player.on("end", async (data: TrackEndEvent) => {
if (this.isDead) return;
if (data.reason === "replaced") return;
@@ -65,6 +70,13 @@ export class GuildPlayer {
public get isRecommend(): boolean {
return this.GDB?.options.recommend ?? false;
}
public get position(): number {
if (!this.isPlaying) return 0;
return this.player.position;
}
public get volume(): number {
return this.player.volume;
}
public async addTrack(track: Track, userId: string) {
this.queue.push({ ...track, userId });
@@ -73,7 +85,7 @@ export class GuildPlayer {
await this.playNext();
} else {
// 재생목록 추가되었을때
this.setMsg();
this.setMsg({ player: false, queue: true });
}
}
@@ -85,7 +97,7 @@ export class GuildPlayer {
await this.playNext();
} else {
// 재생목록 추가되었을때
this.setMsg();
this.setMsg({ player: false, queue: true });
}
}
@@ -100,10 +112,10 @@ export class GuildPlayer {
public async setPause() {
if (this.isPaused) {
await this.player.setPaused(false);
this.setMsg();
this.setMsg({ player: true, queue: false });
} else if (this.isPlaying) {
await this.player.setPaused(true);
this.setMsg();
this.setMsg({ player: true, queue: false });
}
}
@@ -122,20 +134,28 @@ export class GuildPlayer {
let nowQueue = this.queue.slice(1);
nowQueue = shuffle(nowQueue);
this.queue = [this.queue[0], ...nowQueue];
this.setMsg();
this.setMsg({ player: false, queue: true });
}
public setRecommend() {
if (!this.GDB) return;
this.GDB.options.recommend = !this.GDB.options.recommend;
DB.guild.update(this.GDB);
this.setMsg();
this.setMsg({ player: false, queue: false });
}
public setVolume(volume: number) {
if (!this.isPlaying) return;
if (!this.nowTrack) return;
this.player.setGlobalVolume(volume);
this.setMsg({ player: true, queue: false });
}
public seek(num: number) {
if (!this.isPlaying) return;
if (!this.nowTrack) return;
this.player.seekTo(num);
Redis.publishState("player_update", { guildId: this.guild.id });
}
private async autoPlay() {
@@ -189,7 +209,9 @@ export class GuildPlayer {
lavalinkManager.shoukaku.leaveVoiceChannel(this.guild.id);
}
private async setMsg() {
public async setMsg(update: { player: boolean; queue: boolean; } = { player: true, queue: true }) {
if (update.player) Redis.publishState("player_update", { guildId: this.guild.id });
if (update.queue) Redis.publishState("queue_update", { guildId: this.guild.id });
const { channel, msg, check } = await checkTextChannelAndMsg(this.guild, this.textChannel, this.msg);
if (!check) return;
this.textChannel = channel;
@@ -247,7 +269,7 @@ export class GuildPlayer {
files: embed.data.image?.url === default_embed(this.guild.id).data.image?.url ? [ default_image ] : [],
components: [ getButtons(
this.isPlaying || this.isPaused,
this.isPaused,
this.isPlaying && this.isPaused,
realQueue.length > 1,
) ]
});

View File

@@ -6,8 +6,21 @@ import { Spotify } from "../utils/api/Spotify";
import { lavalinkManager } from "../index";
import { getGuildById, getVoiceChannelById } from "../utils/music/Channel";
import { channelJoin } from "../commands/join";
import { GuildPlayer } from "./GuildPlayer";
import { Guild, VoiceChannel } from "discord.js";
type SubAction = "search" | "player_play" | "player_playlist";
type SubAction =
"search" |
"player_now" |
"player_play" |
"player_playlist" |
"player_paused" |
"player_skip" |
"player_seek" |
"player_volume" |
"queue_list" |
"queue_set" |
"queue_remove";
export class RedisClient {
public pub: Redis = new Redis({ host: Config.redis.host, port: Config.redis.port });
@@ -38,7 +51,7 @@ export class RedisClient {
await this.pub.setex(resultKey, 60, JSON.stringify(results));
Logger.log(`[Redis Pub] [setex] 결과 저장: (${resultKey})`);
}
else if (data.action === "player_play") {
if (data.action === "player_play") {
const resultKey = `player:play:${data.requestId}`;
if (!data.serverId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
if (!data.userId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "userId를 찾을수 없습니다." }));
@@ -52,10 +65,10 @@ export class RedisClient {
}
if (!player) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "세션을 찾을수 없습니다." }));
await lavalinkManager.search(guild.id, data.track.url, data.userId, player);
// await this.pub.setex(resultKey, 60, JSON.stringify({ success: true, message: "노래 추가 완료" }));
await this.pub.setex(resultKey, 60, JSON.stringify({ success: true, message: "노래 추가 완료" }));
}
else if (data.action === "player_playlist") {
const resultKey = `player:play:${data.requestId}`;
if (data.action === "player_playlist") {
const resultKey = `player:playlist:${data.requestId}`;
if (!data.serverId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
if (!data.userId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "userId를 찾을수 없습니다." }));
const guild = await getGuildById(data.serverId);
@@ -68,7 +81,112 @@ export class RedisClient {
}
if (!player) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "세션을 찾을수 없습니다." }));
await lavalinkManager.search(guild.id, data.playlistUrl, data.userId, player);
// await this.pub.setex(resultKey, 60, JSON.stringify({ success: true, message: "플레이리스트 추가 완료" }));
await this.pub.setex(resultKey, 60, JSON.stringify({ success: true, message: "플레이리스트 추가 완료" }));
}
if (data.action === "player_now") {
const resultKey = `player:now:${data.requestId}`;
if (!data.serverId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
if (!data.userId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "userId를 찾을수 없습니다." }));
const player = lavalinkManager.getPlayer(data.serverId);
// if (!player) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "player를 찾을수 없습니다." }));
await this.pub.setex(resultKey, 60, JSON.stringify({
success: true,
botPlayer: !!player,
isPlaying: player?.isPlaying,
isPaused: player?.isPaused,
position: player?.position,
volume: player?.volume,
track: player?.nowTrack ?? null
}));
}
if (data.action === "queue_list") {
const resultKey = `queue:list:${data.requestId}`;
if (!data.serverId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
if (!data.userId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "userId를 찾을수 없습니다." }));
const player = lavalinkManager.getPlayer(data.serverId);
// if (!player) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "player를 찾을수 없습니다." }));
await this.pub.setex(resultKey, 60, JSON.stringify({ success: true, queue: player?.queue?.slice(1) ?? [] }));
}
if (data.action === "queue_set") {
const resultKey = `queue:set:${data.requestId}`;
if (!data.serverId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
if (!data.userId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "userId를 찾을수 없습니다." }));
if (!data.newQueue) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "newQueue를 찾을수 없습니다." }));
const context = await this.getContext(data.serverId, resultKey, data.userId);
if (!context.ok) return;
const nowTrack = context.player.nowTrack ?? context.player.queue?.[0];
context.player.queue.length = 0;
if (nowTrack) context.player.queue.push(nowTrack);
for (const rawTrack of data.newQueue) {
if (rawTrack.encoded) context.player.queue.push(rawTrack);
}
await this.pub.setex(resultKey, 60, JSON.stringify({ success: true }));
context.player.setMsg();
}
if (data.action === "queue_remove") {
const resultKey = `queue:remove:${data.requestId}`;
if (!data.serverId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
if (!data.userId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "userId를 찾을수 없습니다." }));
if (!data.index) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "index를 찾을수 없습니다." }));
const context = await this.getContext(data.serverId, resultKey, data.userId);
if (!context.ok) return;
const numIndex = Number(data.index);
if (isNaN(numIndex)) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "index 타입이 올바르지 않습니다." }));
if (numIndex < 0) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "index는 0보다 크거나 같아야합니다." }));
if (numIndex >= context.player.queue.length-1) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "index가 queue.length보다 클수 없습니다." }));
const [removedTrack] = context.player.queue.splice(numIndex+1, 1);
await this.pub.setex(resultKey, 60, JSON.stringify({ success: true, removedTrack }));
context.player.setMsg();
}
if (data.action === "player_paused") {
const resultKey = `player:paused:${data.requestId}`;
if (!data.serverId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
if (!data.userId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "userId를 찾을수 없습니다." }));
if (!data.isPaused) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "paused를 찾을수 없습니다." }));
const context = await this.getContext(data.serverId, resultKey, data.userId);
if (!context.ok) return;
await context.player.setPause();
await this.pub.setex(resultKey, 60, JSON.stringify({ success: true, paused: context.player.isPaused }));
}
if (data.action === "player_skip") {
const resultKey = `player:skip:${data.requestId}`;
if (!data.serverId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
if (!data.userId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "userId를 찾을수 없습니다." }));
const context = await this.getContext(data.serverId, resultKey, data.userId);
if (!context.ok) return;
if (context.player.isPlaying) context.player.skip();
await this.pub.setex(resultKey, 60, JSON.stringify({ success: true }));
}
if (data.action === "player_seek") {
const resultKey = `player:seek:${data.requestId}`;
if (!data.serverId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
if (!data.userId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "userId를 찾을수 없습니다." }));
if (!data.seek) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "seek를 찾을수 없습니다." }));
const context = await this.getContext(data.serverId, resultKey, data.userId);
if (!context.ok) return;
if (!context.player.isPlaying || !context.player.nowTrack) await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "재생중인 노래가 없습니다." }));
const duration = context.player.nowTrack?.info.length || 0;
const numSeek = Number(data.seek);
if (isNaN(numSeek)) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "seek 타입이 올바르지 않습니다." }));
if (numSeek < 0) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "seek는 0보다 크거나 같아야합니다." }));
if (numSeek > duration) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "seek가 duration보다 클수 없습니다." }));
context.player.seek(numSeek);
await this.pub.setex(resultKey, 60, JSON.stringify({ success: true }));
}
if (data.action === "player_volume") {
const resultKey = `player:volume:${data.requestId}`;
if (!data.serverId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
if (!data.userId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "userId를 찾을수 없습니다." }));
if (!data.volume) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "volume을 찾을수 없습니다." }));
const context = await this.getContext(data.serverId, resultKey, data.userId);
if (!context.ok) return;
if (!context.player.isPlaying || !context.player.nowTrack) await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "재생중인 노래가 없습니다." }));
const numVolume = Number(data.volume);
if (isNaN(numVolume)) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "volume 타입이 올바르지 않습니다." }));
if (numVolume < 0) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "volume은 0보다 크거나 같아야합니다." }));
if (numVolume > 100) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "volume이 100보다 클수 없습니다." }));
context.player.setVolume(numVolume);
await this.pub.setex(resultKey, 60, JSON.stringify({ success: true }));
}
} catch (err) {
Logger.error(`명령어 처리 중 에러: ${String(err)}`);
@@ -93,6 +211,38 @@ export class RedisClient {
Logger.log(`[Redis Pub] bot -> site 전송: ${event}`);
}
private async getContext(guildId: string, resultKey: string, userId: string): Promise<{
ok: true;
guild: Guild;
voiceChannel: VoiceChannel | null;
player: GuildPlayer;
} | { ok: false; }> {
const guild = await getGuildById(guildId);
if (!guild) {
await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "guild를 찾을수 없습니다." }));
return { ok: false };
}
let player = lavalinkManager.getPlayer(guild.id);
const voiceChannel = await getVoiceChannelById(guild, userId);
if (!player) {
if (!voiceChannel) {
await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "음성채널에 들어가서 이용해주세요." }));
return { ok: false };
}
player = (await channelJoin(guild, voiceChannel.id)).player;
}
if (!player) {
await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "player를 찾을수 없습니다." }));
return { ok: false };
}
return {
ok: true,
guild,
voiceChannel,
player,
};
}
public runTest() {
Logger.debug(`[Redis Test] 3초 뒤에 테스트 통신 시작...`);
setTimeout(() => {

View File

@@ -124,7 +124,7 @@ export const Spotify = {
const lowerQuery = query.toLocaleLowerCase().trim();
if (searchCache.has(lowerQuery)) return searchCache.get(lowerQuery) ?? null;
const track = (await this.getSearchFull(query) ?? [])?.[0];
if (track.url) searchCache.set(lowerQuery, track.url);
return track.url;
if (track?.url) searchCache.set(lowerQuery, track.url);
return track?.url ?? null;
}
}

View File

@@ -211,7 +211,7 @@ export const YoutubeMusic = {
const lowerQuery = query.toLocaleLowerCase().trim();
if (searchCache.has(lowerQuery)) return searchCache.get(lowerQuery) ?? null;
const video = (await this.getSearchFull(query) ?? [])?.[0];
if (video.url) searchCache.set(lowerQuery, video.url);
return video.url;
if (video?.url) searchCache.set(lowerQuery, video.url);
return video?.url ?? null;
}
};