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:
@@ -2,13 +2,13 @@
|
|||||||
"name": "tts_bot",
|
"name": "tts_bot",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "discord bot with typescript",
|
"description": "discord bot with typescript",
|
||||||
"homepage": "https://github.com/tkrmagid/bot.ts#readme",
|
"homepage": "https://git.tkrmagid.kr/tkrmagid/tts_bot",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/tkrmagid/bot.ts/issues"
|
"url": "https://git.tkrmagid.kr/tkrmagid/tts_bot/issues"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/tkrmagid/bot.ts.git"
|
"url": "git+https://git.tkrmagid.kr/tkrmagid/tts_bot.git"
|
||||||
},
|
},
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"author": "tkrmagid",
|
"author": "tkrmagid",
|
||||||
|
|||||||
@@ -1,9 +1,23 @@
|
|||||||
import colors from "colors/safe";
|
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 = () => {
|
export const Timestamp = () => {
|
||||||
const Now = new Date();
|
const parts = KST_FORMATTER.formatToParts(new Date());
|
||||||
Now.setHours(Now.getHours() + 9);
|
const get = (type: string) => parts.find(p => p.type === type)?.value ?? "00";
|
||||||
return Now.toISOString().replace('T', ' ').substring(0, 19).slice(2);
|
return `${get("year")}-${get("month")}-${get("day")} ${get("hour")}:${get("minute")}:${get("second")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
type logType = "log" | "info" | "warn" | "error" | "debug" | "ready" | "slash";
|
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 { ChildProcessWithoutNullStreams, spawn } from "node:child_process";
|
||||||
import { Readable } from "node:stream";
|
import { Readable } from "node:stream";
|
||||||
import { AudioResource, createAudioResource, StreamType } from "@discordjs/voice";
|
import { AudioResource, createAudioResource, StreamType } from "@discordjs/voice";
|
||||||
|
import { Logger } from "./Logger";
|
||||||
|
|
||||||
/** MP3 Buffer -> PCM(s16le 48k 2ch) Readable stream */
|
/** MP3 Buffer -> PCM(s16le 48k 2ch) Readable stream */
|
||||||
function mp3BufferToPcmStream(mp3Buf: Buffer): Readable {
|
function mp3BufferToPcmStream(mp3Buf: Buffer): Readable {
|
||||||
if (!ffmpegPath) throw new Error("ffmpeg-static 경로 확인 실패");
|
if (!ffmpegPath) throw new Error("ffmpeg-static 경로 확인 실패");
|
||||||
const ff: ChildProcessWithoutNullStreams = spawn(ffmpegPath, [
|
const ff: ChildProcessWithoutNullStreams = spawn(ffmpegPath, [
|
||||||
"-loglevel","quiet","-hide_banner",
|
"-loglevel","error","-hide_banner",
|
||||||
"-i","pipe:0", // stdin으로 mp3
|
"-i","pipe:0", // stdin으로 mp3
|
||||||
"-f","s16le", // raw PCM
|
"-f","s16le", // raw PCM
|
||||||
"-ar","48000", // 48k
|
"-ar","48000", // 48k
|
||||||
@@ -15,14 +16,20 @@ function mp3BufferToPcmStream(mp3Buf: Buffer): Readable {
|
|||||||
"pipe:1" // stdout으로 PCM
|
"pipe:1" // stdout으로 PCM
|
||||||
], { stdio: ["pipe","pipe","pipe"] });
|
], { stdio: ["pipe","pipe","pipe"] });
|
||||||
|
|
||||||
// 입력 밀어넣고 닫기
|
// ffmpeg가 비정상 종료해도 부모가 죽지 않도록 모든 핸들에 에러 핸들러를 단다.
|
||||||
// ff.stdin.write(mp3Buf);
|
// stdin EPIPE, stdout 소비 실패 등을 조용히 흘려 보내고 자식 프로세스를
|
||||||
// ff.stdin.end();
|
// 강제 종료해 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;
|
return ff.stdout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user