chore: harden logger TZ, ffmpeg lifecycle and fix package metadata
- Logger.Timestamp now formats via Intl with timeZone Asia/Seoul, so the timestamp is correct regardless of the container/host TZ. The previous setHours(+9) hack assumed the system clock was UTC. - Transcode.mp3BufferToPcmStream now attaches error/stderr handlers to the ffmpeg child process and its streams, swallows EPIPE on early downstream close, and force-kills on spawn error so failed conversions can't leak processes. Log level bumped from 'quiet' to 'error' so real ffmpeg errors surface. - package.json homepage/bugs/repository pointed at github.com/tkrmagid/bot.ts which doesn't reflect this repo. Repoint to the actual Gitea origin.
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user