feat: 쿠키 파일 기반 설정(YOUTUBE_COOKIE_PATH)+리졸버 URL 설정화, 다음곡 프리페치로 재생지연 단축, ytresolver 리포 포함, 쿠키 재발급 스크립트, README 상세화

This commit is contained in:
tkrmagid
2026-09-11 02:34:55 +09:00
parent e198c5244a
commit 56bea22e4a
7 changed files with 472 additions and 21 deletions

View File

@@ -8,6 +8,7 @@ import { DB } from "../utils/Database";
import { shuffle } from "../utils/Shuffle";
import { checkTextChannelAndMsg } from "../utils/music/Channel";
import { Logger } from "../utils/Logger";
import { Config } from "../utils/Config";
const DelayAfterErrMs = 1000 * 5;
const idleEndTime = 1000 * 60 * 10;
@@ -17,7 +18,9 @@ const StallThresholdMs = 1000 * 20;
const MaxLoadFailRetry = 3;
const LoadFailRetryDelayMs = 2500;
type QueueTrack = Track & { userId: string; };
// _resolved: yt-dlp 리졸브 결과(http 소스 encoded)를 미리 받아두는 프리페치 캐시.
// 다음 곡을 재생 전에 미리 리졸브해 두어 곡 전환 시 대기시간을 없앤다.
type QueueTrack = Track & { userId: string; _resolved?: Promise<string>; };
export class GuildPlayer {
private isDead = false;
@@ -53,6 +56,8 @@ export class GuildPlayer {
}
// 새 곡이 시작되면 재생 위치 멈춤 감시 시작
this.startStallWatchdog();
// 현재 곡이 재생되는 동안 다음 곡을 미리 리졸브(곡 전환 지연 제거)
this.prefetch(this.queue[1]);
Redis?.publishState("player_update", {
guildId: this.guild.id,
});
@@ -232,7 +237,8 @@ export class GuildPlayer {
// 큐가 비어있으면 바로 재생
await this.playNext();
} else {
// 재생목록 추가되었을때
// 재생목록 추가되었을때: 다음 재생될 곡을 미리 리졸브
this.prefetch(this.queue[1]);
this.setMsg({ player: false, queue: true });
}
}
@@ -241,10 +247,12 @@ export class GuildPlayer {
const first = this.queue.length === 0;
for (const track of tracks) this.queue.push({ ...track, userId });
if (first) {
// 큐가 비어있으면 바로 재생
// 큐가 비어있으면 바로 재생 (+ 다음 곡 미리 리졸브)
this.prefetch(this.queue[1]);
await this.playNext();
} else {
// 재생목록 추가되었을때
// 재생목록 추가되었을때: 다음 재생될 곡을 미리 리졸브
this.prefetch(this.queue[1]);
this.setMsg({ player: false, queue: true });
}
}
@@ -258,8 +266,7 @@ export class GuildPlayer {
if (src !== "youtube" && src !== "spotify" && src !== "youtubemusic") return track.encoded;
const target = (src === "youtube" && info.uri) ? info.uri : `${info.author ?? ""} ${info.title ?? ""}`.trim();
if (!target) return track.encoded;
const host = process.env.LAVALINK_HOST || "192.168.10.6";
const r = await fetch(`http://${host}:8779/resolve?url=${encodeURIComponent(target)}`, { signal: AbortSignal.timeout(45000) });
const r = await fetch(`${Config.ytresolverUrl}/resolve?url=${encodeURIComponent(target)}`, { signal: AbortSignal.timeout(45000) });
if (!r.ok) return track.encoded;
const data = (await r.json()) as { url?: string };
if (!data.url) return track.encoded;
@@ -274,10 +281,28 @@ export class GuildPlayer {
}
}
/** 트랙의 재생용 encoded를 미리(백그라운드) 리졸브해 캐시한다. 이미 진행 중이면 무시. */
private prefetch(track?: QueueTrack) {
if (!track || track._resolved) return;
track._resolved = this.resolvePlayable(track).catch((e) => {
Logger.warn(`[GuildPlayer] 프리페치 실패(재생 시 재시도): ${String(e)}`);
track._resolved = undefined; // 실패 시 캐시 비워 재생 시점에 다시 시도
return track.encoded;
});
}
/** 재생용 encoded를 가져온다. 프리페치되어 있으면 즉시, 아니면 지금 리졸브한다. */
private getResolved(track: QueueTrack): Promise<string> {
if (!track._resolved) this.prefetch(track);
return track._resolved!;
}
public async playNext() {
const track = this.queue[0];
if (!track) return this.end();
const encoded = await this.resolvePlayable(track);
const encoded = await this.getResolved(track);
// 다음 곡을 미리 리졸브해 곡 전환 지연 제거
this.prefetch(this.queue[1]);
await this.player.playTrack({ track: { encoded } });
// 재생관련
this.setMsg();