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:
Claude Owner
2026-05-26 14:41:29 +09:00
parent 35569ddd88
commit acdaa4734f
3 changed files with 34 additions and 13 deletions

View File

@@ -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",

View File

@@ -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";

View File

@@ -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;
}