지금까지 내용 커밋

This commit is contained in:
2026-04-08 12:59:45 +09:00
commit b0dae31cb9
68 changed files with 12083 additions and 0 deletions

View File

@@ -0,0 +1,278 @@
import { Guild, Message, TextChannel } from "discord.js";
import { LoadType, Player, Track, TrackEndEvent } from "shoukaku";
import { client, lavalinkManager } 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";
import { DB } from "../utils/Database";
import { shuffle } from "../utils/Shuffle";
import { checkTextChannelAndMsg } from "../utils/music/Channel";
const DelayAfterErrMs = 1000 * 5;
const idleEndTime = 1000 * 60 * 10;
type QueueTrack = Track & { userId: string; };
export class GuildPlayer {
private isDead = false;
private canRecommend = true;
private _GDB: GuildType | undefined;
private lastPlayedTrack: QueueTrack | undefined;
public queue: QueueTrack[] = [];
private errorTimer: NodeJS.Timeout | undefined;
private endTimer: NodeJS.Timeout | undefined;
constructor(
public guild: Guild,
public player: Player,
public voiceChannelId: string,
public textChannel: TextChannel,
public msg: Message,
) {
this.player.setGlobalVolume(50);
this.player.on("end", async (data: TrackEndEvent) => {
if (this.isDead) return;
if (data.reason === "replaced") return;
// 방금 끝난 곡을 대기열에서 지우면서 lastPlayedTrack에 저장
this.lastPlayedTrack = this.queue.shift();
if (this.queue.length > 0) {
await this.playNext();
} else if (this.isRecommend && this.canRecommend) {
await this.autoPlay();
} else {
this.end();
}
});
this.player.on("closed", () => {
if (this.isDead) return;
this.delete();
});
}
private get GDB() {
if (!this._GDB) this._GDB = DB.guild.get(this.guild.id);
return this._GDB;
}
public get isPlaying(): boolean {
return this.player.track !== null;
}
public get nowTrack(): QueueTrack | null {
return this.queue[0] ?? null;
}
public get isPaused(): boolean {
return this.player.paused;
}
public get isRecommend(): boolean {
return this.GDB?.options.recommend ?? false;
}
public async addTrack(track: Track, userId: string) {
this.queue.push({ ...track, userId });
if (this.queue.length === 1) {
// 큐가 비어있으면 바로 재생
await this.playNext();
} else {
// 재생목록 추가되었을때
this.setMsg();
}
}
public async addTracks(tracks: Track[], userId: string) {
const first = this.queue.length === 0;
for (const track of tracks) this.queue.push({ ...track, userId });
if (first) {
// 큐가 비어있으면 바로 재생
await this.playNext();
} else {
// 재생목록 추가되었을때
this.setMsg();
}
}
public async playNext() {
const track = this.queue[0];
if (!track) return this.end();
await this.player.playTrack({ track: { encoded: track.encoded } });
// 재생관련
this.setMsg();
}
public async setPause() {
if (this.isPaused) {
await this.player.setPaused(false);
this.setMsg();
} else if (this.isPlaying) {
await this.player.setPaused(true);
this.setMsg();
}
}
public stop() {
this.queue = [];
this.canRecommend = false;
this.player.stopTrack();
}
public skip() {
// 곡 스킵했을때
this.player.stopTrack();
}
public setShuffle() {
let nowQueue = this.queue.slice(1);
nowQueue = shuffle(nowQueue);
this.queue = [this.queue[0], ...nowQueue];
this.setMsg();
}
public setRecommend() {
if (!this.GDB) return;
this.GDB.options.recommend = !this.GDB.options.recommend;
DB.guild.update(this.GDB);
this.setMsg();
}
public seek(num: number) {
if (!this.isPlaying) return;
if (!this.nowTrack) return;
this.player.seekTo(num);
}
private async autoPlay() {
if (!this.lastPlayedTrack) return;
const source = this.lastPlayedTrack.info.sourceName;
const trackId = this.lastPlayedTrack.info.identifier;
let result;
if (source === "spotify") {
result = await this.player.node.rest.resolve(`sprec:seed_tracks=${trackId}&limit=11`);
} else {
result = await this.player.node.rest.resolve(`https://music.youtube.com/watch?v=${trackId}&list=RD${trackId}`);
}
let tracks: Track[] = [];
if (result?.loadType === LoadType.PLAYLIST) {
tracks = result.data.tracks;
} else if (result?.loadType === LoadType.SEARCH || Array.isArray(result?.data)) {
tracks = Array.isArray(result.data) ? result.data : [];
} else if (result?.loadType === LoadType.TRACK) {
tracks = [ result.data ];
}
if (tracks.length === 0) {
this.end();
return;
}
if (tracks[0].info.identifier === trackId) tracks = tracks.slice(1);
this.addTracks(tracks, "자동재생");
}
public end() {
if (this.errorTimer !== undefined) {
clearTimeout(this.errorTimer);
this.errorTimer = undefined;
}
if (this.endTimer !== undefined) clearTimeout(this.endTimer);
this.endTimer = setTimeout(() => {
this.endTimer = undefined;
this.delete(true);
}, idleEndTime);
this.lastPlayedTrack = undefined;
this.queue = [];
this.canRecommend = true;
this.setMsg();
}
public delete(afterEnd?: boolean) {
if (this.isDead) return;
if (!afterEnd) this.end();
this.isDead = true;
this.player.destroy().catch(() => {});
lavalinkManager.delPlayer(this.guild.id);
lavalinkManager.shoukaku.leaveVoiceChannel(this.guild.id);
}
private async setMsg() {
const { channel, msg, check } = await checkTextChannelAndMsg(this.guild, this.textChannel, this.msg);
if (!check) return;
this.textChannel = channel;
this.msg = msg;
if (!this.msg) return;
// content
let content = default_content;
let realQueue = this.queue.slice(1);
if (realQueue.length > 0) {
content = "__**대기열 목록:**__\n";
let list: string[] = [];
let nowLength = content.length + 50; // 50자 여유두기
for (let i=0; i<realQueue.length; i++) {
let track = realQueue[i];
let text = `${
(i+1).toString().padStart(realQueue.length.toString().length, "0")
}\\. ${
track.info.author.replace(" - Topic","")
} - ${
track.info.title
} [${
timeFormat(track.info.length)
}] ~ ${
track.userId === "자동재생" ? "자동재생" : `<@${track.userId}>`
}`;
if (nowLength + text.length > 2000) {
content += `+ ${realQueue.length-list.length}곡\n`;
break;
}
nowLength += text.length;
list.push(text);
}
content += list.reverse().join("\n");
}
// embed
const embed = (this.isPlaying || this.isPaused) && !!this.nowTrack
? client.mkembed({
title: `**[${timeFormat(this.nowTrack.info.length)}] ${this.nowTrack.info.author.replace(" - Topic","")} - ${this.nowTrack.info.title}**`,
description: `노래 요청자: ${this.nowTrack.userId === "자동재생" ? "자동재생" : `<@${this.nowTrack.userId}>`}`,
image: this.nowTrack.info.artworkUrl,
url: this.nowTrack.info.author.includes(" - Topic") ? this.nowTrack.info.uri?.replace("www.youtube","music.youtube") : this.nowTrack.info.uri,
footer: { text: `대기열: ${realQueue.length}개 | 볼륨: ${this.player.volume}${
this.isRecommend ? " | 자동재생: 활성화" : ""
}${
this.isPaused ? " | 노래가 일시중지 되었습니다." : ""
}` }
})
: default_embed(this.guild.id);
this.msg.edit({
content: content,
embeds: [ embed ],
files: embed.data.image?.url === default_embed(this.guild.id).data.image?.url ? [ default_image ] : [],
components: [ getButtons(
this.isPlaying || this.isPaused,
this.isPaused,
realQueue.length > 1,
) ]
});
}
public async errMsg(reason: string) {
if (!this.msg) return null;
await this.msg.edit({
content: this.msg.content,
embeds: [ client.mkembed({
title: "오류발생!!",
description: reason,
color: "DarkRed",
}) ],
files: [],
components: [ getButtons(
false,
false,
this.queue.length > 1,
) ]
});
this.errorTimer = setTimeout(() => {
this.errorTimer = undefined;
this.playNext();
}, DelayAfterErrMs);
return null;
}
}