diff --git a/package.json b/package.json index 62403de..c162db7 100644 --- a/package.json +++ b/package.json @@ -2,13 +2,13 @@ "name": "tts_bot", "version": "0.0.1", "description": "discord bot with typescript", - "homepage": "https://github.com/tkrmagid/bot.ts#readme", + "homepage": "https://git.tkrmagid.kr/tkrmagid/tts_bot", "bugs": { - "url": "https://github.com/tkrmagid/bot.ts/issues" + "url": "https://git.tkrmagid.kr/tkrmagid/tts_bot/issues" }, "repository": { "type": "git", - "url": "git+https://github.com/tkrmagid/bot.ts.git" + "url": "git+https://git.tkrmagid.kr/tkrmagid/tts_bot.git" }, "license": "ISC", "author": "tkrmagid", diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index 9d4ace1..074ab94 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -1,9 +1,23 @@ import colors from "colors/safe"; +// 컨테이너/호스트의 TZ 설정과 무관하게 항상 KST(Asia/Seoul) 기준으로 +// 'YY-MM-DD HH:MM:SS' 형식의 타임스탬프를 만든다. 기존 구현은 +// setHours(+9)로 수동 보정해 서버 TZ가 UTC가 아닐 때 시간이 어긋났다. +const KST_FORMATTER = new Intl.DateTimeFormat("en-CA", { + timeZone: "Asia/Seoul", + year: "2-digit", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, +}); + export const Timestamp = () => { - const Now = new Date(); - Now.setHours(Now.getHours() + 9); - return Now.toISOString().replace('T', ' ').substring(0, 19).slice(2); + const parts = KST_FORMATTER.formatToParts(new Date()); + const get = (type: string) => parts.find(p => p.type === type)?.value ?? "00"; + return `${get("year")}-${get("month")}-${get("day")} ${get("hour")}:${get("minute")}:${get("second")}`; } type logType = "log" | "info" | "warn" | "error" | "debug" | "ready" | "slash"; diff --git a/src/utils/Transcode.ts b/src/utils/Transcode.ts index 4080df1..e724343 100644 --- a/src/utils/Transcode.ts +++ b/src/utils/Transcode.ts @@ -2,12 +2,13 @@ import ffmpegPath from "ffmpeg-static"; import { ChildProcessWithoutNullStreams, spawn } from "node:child_process"; import { Readable } from "node:stream"; import { AudioResource, createAudioResource, StreamType } from "@discordjs/voice"; +import { Logger } from "./Logger"; /** MP3 Buffer -> PCM(s16le 48k 2ch) Readable stream */ function mp3BufferToPcmStream(mp3Buf: Buffer): Readable { if (!ffmpegPath) throw new Error("ffmpeg-static 경로 확인 실패"); const ff: ChildProcessWithoutNullStreams = spawn(ffmpegPath, [ - "-loglevel","quiet","-hide_banner", + "-loglevel","error","-hide_banner", "-i","pipe:0", // stdin으로 mp3 "-f","s16le", // raw PCM "-ar","48000", // 48k @@ -15,14 +16,20 @@ function mp3BufferToPcmStream(mp3Buf: Buffer): Readable { "pipe:1" // stdout으로 PCM ], { stdio: ["pipe","pipe","pipe"] }); - // 입력 밀어넣고 닫기 - // ff.stdin.write(mp3Buf); - // ff.stdin.end(); + // ffmpeg가 비정상 종료해도 부모가 죽지 않도록 모든 핸들에 에러 핸들러를 단다. + // stdin EPIPE, stdout 소비 실패 등을 조용히 흘려 보내고 자식 프로세스를 + // 강제 종료해 leak을 막는다. + const killFf = () => { try { ff.kill("SIGKILL"); } catch {} }; + ff.on("error", (e) => { Logger.error("ffmpeg spawn error: "+String(e)); killFf(); }); + ff.stderr.on("data", (chunk: Buffer) => { + const msg = chunk.toString().trim(); + if (msg) Logger.warn("[ffmpeg] "+msg); + }); + ff.stdin.on("error", () => { /* EPIPE on early close */ }); + ff.stdout.on("error", () => { /* downstream gone */ }); - Readable.from(mp3Buf).pipe(ff.stdin); + Readable.from(mp3Buf).pipe(ff.stdin).on("error", () => {}); - // ffmpeg stdout(PCM)을 그대로 리턴 - // (에러 로그가 필요하면 ff.stderr 'data' 핸들링 추가) return ff.stdout; }