@sayue_ltr/fleq 1.50.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +174 -0
- package/README.md +42 -6
- package/dist/config.js +37 -4
- package/dist/dmdata/rest-client.js +58 -3
- package/dist/dmdata/telegram-parser.js +115 -64
- package/dist/dmdata/ws-client.js +49 -18
- package/dist/engine/cli/cli-run.js +88 -3
- package/dist/engine/cli/cli.js +12 -0
- package/dist/engine/eew/eew-tracker.js +41 -15
- package/dist/engine/filter/compile-filter.js +21 -0
- package/dist/engine/filter/compiler.js +188 -0
- package/dist/engine/filter/errors.js +41 -0
- package/dist/engine/filter/field-registry.js +78 -0
- package/dist/engine/filter/index.js +15 -0
- package/dist/engine/filter/parser.js +137 -0
- package/dist/engine/filter/rank-maps.js +34 -0
- package/dist/engine/filter/tokenizer.js +121 -0
- package/dist/engine/filter/type-checker.js +104 -0
- package/dist/engine/filter/types.js +2 -0
- package/dist/engine/filter-template/pipeline-controller.js +73 -0
- package/dist/engine/filter-template/pipeline.js +16 -0
- package/dist/engine/messages/display-callbacks.js +7 -0
- package/dist/engine/messages/message-router.js +114 -182
- package/dist/engine/messages/summary-tracker.js +106 -0
- package/dist/engine/messages/telegram-stats.js +103 -0
- package/dist/engine/messages/volcano-route-handler.js +122 -0
- package/dist/engine/monitor/monitor.js +52 -4
- package/dist/engine/monitor/shutdown.js +1 -0
- package/dist/engine/notification/notifier.js +21 -4
- package/dist/engine/notification/sound-player.js +398 -36
- package/dist/engine/presentation/diff-store.js +158 -0
- package/dist/engine/presentation/diff-types.js +2 -0
- package/dist/engine/presentation/events/from-earthquake.js +53 -0
- package/dist/engine/presentation/events/from-eew.js +72 -0
- package/dist/engine/presentation/events/from-lg-observation.js +58 -0
- package/dist/engine/presentation/events/from-nankai-trough.js +39 -0
- package/dist/engine/presentation/events/from-raw.js +35 -0
- package/dist/engine/presentation/events/from-seismic-text.js +37 -0
- package/dist/engine/presentation/events/from-tsunami.js +51 -0
- package/dist/engine/presentation/events/from-volcano.js +88 -0
- package/dist/engine/presentation/events/to-presentation-event.js +32 -0
- package/dist/engine/presentation/level-helpers.js +118 -0
- package/dist/engine/presentation/processors/process-earthquake.js +36 -0
- package/dist/engine/presentation/processors/process-eew.js +105 -0
- package/dist/engine/presentation/processors/process-lg-observation.js +30 -0
- package/dist/engine/presentation/processors/process-message.js +53 -0
- package/dist/engine/presentation/processors/process-nankai-trough.js +30 -0
- package/dist/engine/presentation/processors/process-raw.js +22 -0
- package/dist/engine/presentation/processors/process-seismic-text.js +30 -0
- package/dist/engine/presentation/processors/process-tsunami.js +42 -0
- package/dist/engine/presentation/processors/process-volcano.js +41 -0
- package/dist/engine/presentation/types.js +2 -0
- package/dist/engine/startup/config-resolver.js +2 -0
- package/dist/engine/template/compile-template.js +18 -0
- package/dist/engine/template/compiler.js +105 -0
- package/dist/engine/template/field-accessor.js +31 -0
- package/dist/engine/template/filters.js +100 -0
- package/dist/engine/template/index.js +5 -0
- package/dist/engine/template/parser.js +185 -0
- package/dist/engine/template/tokenizer.js +96 -0
- package/dist/engine/template/types.js +2 -0
- package/dist/types.js +3 -2
- package/dist/ui/display-adapter.js +60 -0
- package/dist/ui/earthquake-formatter.js +22 -5
- package/dist/ui/eew-formatter.js +25 -10
- package/dist/ui/formatter.js +116 -32
- package/dist/ui/minimap/grid-layout.js +91 -0
- package/dist/ui/minimap/index.js +16 -0
- package/dist/ui/minimap/minimap-renderer.js +277 -0
- package/dist/ui/minimap/pref-mapping.js +82 -0
- package/dist/ui/minimap/types.js +2 -0
- package/dist/ui/night-overlay.js +56 -0
- package/dist/ui/repl-handlers/command-definitions.js +327 -0
- package/dist/ui/repl-handlers/index.js +11 -0
- package/dist/ui/repl-handlers/info-handlers.js +633 -0
- package/dist/ui/repl-handlers/operation-handlers.js +233 -0
- package/dist/ui/repl-handlers/settings-handlers.js +927 -0
- package/dist/ui/repl-handlers/types.js +10 -0
- package/dist/ui/repl.js +81 -1752
- package/dist/ui/statistics-formatter.js +258 -0
- package/dist/ui/status-line.js +80 -0
- package/dist/ui/summary/index.js +5 -0
- package/dist/ui/summary/summary-line.js +18 -0
- package/dist/ui/summary/summary-model.js +31 -0
- package/dist/ui/summary/token-builders.js +317 -0
- package/dist/ui/summary/types.js +2 -0
- package/dist/ui/summary/width-fit.js +41 -0
- package/dist/ui/summary-interval-formatter.js +72 -0
- package/dist/ui/test-samples.js +6 -0
- package/dist/ui/theme.js +43 -5
- package/dist/ui/tip-shuffler.js +81 -0
- package/dist/ui/volcano-formatter.js +15 -13
- package/dist/ui/waiting-tips-eew.js +63 -0
- package/dist/ui/waiting-tips-info-systems.js +81 -0
- package/dist/ui/waiting-tips-seismology.js +97 -0
- package/dist/ui/waiting-tips-tsunami.js +72 -0
- package/dist/ui/waiting-tips-weather.js +189 -0
- package/dist/ui/waiting-tips.js +420 -249
- package/package.json +1 -1
|
@@ -35,7 +35,13 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.SOUND_LEVELS = void 0;
|
|
37
37
|
exports.isSoundLevel = isSoundLevel;
|
|
38
|
+
exports.clearCustomSoundCache = clearCustomSoundCache;
|
|
39
|
+
exports._setUptimeProviderForTest = _setUptimeProviderForTest;
|
|
40
|
+
exports._nowMsForTest = _nowMsForTest;
|
|
38
41
|
exports.playSound = playSound;
|
|
42
|
+
exports.dispose = dispose;
|
|
43
|
+
exports.resetSoundPlayer = resetSoundPlayer;
|
|
44
|
+
exports.checkSoundBackend = checkSoundBackend;
|
|
39
45
|
const child_process_1 = require("child_process");
|
|
40
46
|
const fs = __importStar(require("fs"));
|
|
41
47
|
const path = __importStar(require("path"));
|
|
@@ -82,143 +88,446 @@ const LINUX_CANBERRA_EVENTS = {
|
|
|
82
88
|
info: "dialog-information",
|
|
83
89
|
cancel: "bell",
|
|
84
90
|
};
|
|
91
|
+
// ── findCustomSound キャッシュ ──
|
|
92
|
+
/** findCustomSound の結果キャッシュ (null = 存在しない, undefined = 未検索) */
|
|
93
|
+
const customSoundCache = new Map();
|
|
85
94
|
/**
|
|
86
95
|
* カスタム効果音ファイルのパスを返す。見つからなければ null。
|
|
87
|
-
* mp3 → wav
|
|
96
|
+
* mp3 → wav の順で探索する。結果はキャッシュして再利用する。
|
|
88
97
|
*/
|
|
89
98
|
function findCustomSound(level) {
|
|
99
|
+
if (customSoundCache.has(level)) {
|
|
100
|
+
return customSoundCache.get(level);
|
|
101
|
+
}
|
|
90
102
|
const baseName = CUSTOM_SOUND_FILES[level];
|
|
91
103
|
for (const ext of SUPPORTED_EXTENSIONS) {
|
|
92
104
|
const filePath = path.join(CUSTOM_SOUNDS_DIR, baseName + ext);
|
|
93
105
|
if (fs.existsSync(filePath)) {
|
|
106
|
+
customSoundCache.set(level, filePath);
|
|
94
107
|
return filePath;
|
|
95
108
|
}
|
|
96
109
|
}
|
|
110
|
+
customSoundCache.set(level, null);
|
|
97
111
|
return null;
|
|
98
112
|
}
|
|
113
|
+
/** サウンドキャッシュをクリアする (テスト用) */
|
|
114
|
+
function clearCustomSoundCache() {
|
|
115
|
+
customSoundCache.clear();
|
|
116
|
+
windowsSoundCache.clear();
|
|
117
|
+
}
|
|
118
|
+
// ── 起動ウィンドウ用の単調時計 ──
|
|
119
|
+
/** 起動後リトライのウィンドウ (ms) */
|
|
120
|
+
const STARTUP_WINDOW_MS = 60_000;
|
|
121
|
+
/** 失敗時のリトライ待機 (ms) */
|
|
122
|
+
const RETRY_DELAY_MS = 20_000;
|
|
123
|
+
/** テスト用に差し替え可能な uptime プロバイダ (秒) */
|
|
124
|
+
let uptimeProvider = null;
|
|
125
|
+
/** プロセス起動からの経過ミリ秒 (Date.now の非単調性を避ける) */
|
|
126
|
+
function nowMs() {
|
|
127
|
+
const seconds = uptimeProvider != null ? uptimeProvider() : process.uptime();
|
|
128
|
+
return Math.floor(seconds * 1000);
|
|
129
|
+
}
|
|
130
|
+
/** テスト用: uptime プロバイダを差し替える (null で本物に戻す) */
|
|
131
|
+
function _setUptimeProviderForTest(fn) {
|
|
132
|
+
uptimeProvider = fn;
|
|
133
|
+
}
|
|
134
|
+
/** テスト用: 現在の nowMs 値を返す */
|
|
135
|
+
function _nowMsForTest() {
|
|
136
|
+
return nowMs();
|
|
137
|
+
}
|
|
138
|
+
// ── 有界キュー ──
|
|
139
|
+
/** 同時再生の最大数 */
|
|
140
|
+
const MAX_CONCURRENT = 1;
|
|
141
|
+
/** キューの最大サイズ (超えたら古いエントリを破棄) */
|
|
142
|
+
const MAX_QUEUE_SIZE = 3;
|
|
143
|
+
/** 再生プロセスのタイムアウト (ms) */
|
|
144
|
+
const PLAY_TIMEOUT_MS = 10_000;
|
|
145
|
+
/** 再生中のプロセス数 */
|
|
146
|
+
let activeCount = 0;
|
|
147
|
+
/** 再生待ちキュー (level と isRetry フラグを保持) */
|
|
148
|
+
const playQueue = [];
|
|
149
|
+
/** 現在再生中のプロセス (タイムアウト kill 用) */
|
|
150
|
+
let activeProcess = null;
|
|
151
|
+
/** タイムアウトタイマー */
|
|
152
|
+
let activeTimer = null;
|
|
153
|
+
/** 起動後リトライのタイマーハンドル集合 (dispose でクリア) */
|
|
154
|
+
const retryTimers = new Set();
|
|
99
155
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
|
|
156
|
+
* 再生世代カウンタ。dispose/resetSoundPlayer で増やすことで、
|
|
157
|
+
* 旧世代の子プロセスコールバックが戻ってきても completion を拒否する。
|
|
158
|
+
*/
|
|
159
|
+
let runGeneration = 0;
|
|
160
|
+
/** dispose 済みフラグ */
|
|
161
|
+
let disposed = false;
|
|
162
|
+
/**
|
|
163
|
+
* 再生完了後にキューから次のエントリを取り出して再生する。
|
|
164
|
+
*/
|
|
165
|
+
function onPlayFinished() {
|
|
166
|
+
activeCount--;
|
|
167
|
+
activeProcess = null;
|
|
168
|
+
if (activeTimer != null) {
|
|
169
|
+
clearTimeout(activeTimer);
|
|
170
|
+
activeTimer = null;
|
|
171
|
+
}
|
|
172
|
+
if (disposed)
|
|
173
|
+
return;
|
|
174
|
+
if (playQueue.length > 0) {
|
|
175
|
+
const next = playQueue.shift();
|
|
176
|
+
runPlay(next.level, { isRetry: next.isRetry });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* 起動ウィンドウ内で失敗した場合に、指定秒後の再試行をスケジュールする。
|
|
181
|
+
* - リトライ由来の失敗 (opts.isRetry) は再リトライしない
|
|
182
|
+
* - 起動ウィンドウ判定は runPlay 開始時に捕捉済みの値を渡すこと
|
|
183
|
+
*/
|
|
184
|
+
function scheduleRetryIfNeeded(level, opts) {
|
|
185
|
+
if (opts?.isRetry)
|
|
186
|
+
return;
|
|
187
|
+
if (disposed)
|
|
188
|
+
return;
|
|
189
|
+
log.warn(`通知音を ${RETRY_DELAY_MS / 1000} 秒後に再試行します (${level})`);
|
|
190
|
+
const timer = setTimeout(() => {
|
|
191
|
+
retryTimers.delete(timer);
|
|
192
|
+
if (!disposed)
|
|
193
|
+
playSound(level, { isRetry: true });
|
|
194
|
+
}, RETRY_DELAY_MS);
|
|
195
|
+
retryTimers.add(timer);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* プロセスを起動して再生を実行する。
|
|
199
|
+
* タイムアウトと launch 経路のコールバックが競合した場合、先に claim() した
|
|
200
|
+
* 側だけが log/bell/キュー進行を実行する (二重完了ガード)。
|
|
201
|
+
* dispose/resetSoundPlayer で runGeneration が変わると旧 runPlay の完了は拒否される。
|
|
103
202
|
*/
|
|
104
|
-
function
|
|
203
|
+
function runPlay(level, opts) {
|
|
204
|
+
activeCount++;
|
|
205
|
+
let finished = false;
|
|
206
|
+
const myGeneration = runGeneration;
|
|
207
|
+
// リトライ対象判定は runPlay 開始時の uptime で確定させる。
|
|
208
|
+
// 10 秒 timeout などで失敗確定時点でウィンドウ外でも、
|
|
209
|
+
// 起動直後に始まった再生はリトライ対象とする。
|
|
210
|
+
const startedInStartupWindow = nowMs() < STARTUP_WINDOW_MS;
|
|
211
|
+
const handle = {
|
|
212
|
+
claim: () => {
|
|
213
|
+
if (finished)
|
|
214
|
+
return false;
|
|
215
|
+
// dispose/reset された旧世代の runPlay は完了処理を行わない
|
|
216
|
+
if (myGeneration !== runGeneration)
|
|
217
|
+
return false;
|
|
218
|
+
finished = true;
|
|
219
|
+
return true;
|
|
220
|
+
},
|
|
221
|
+
done: (failed) => {
|
|
222
|
+
if (failed === true && startedInStartupWindow) {
|
|
223
|
+
scheduleRetryIfNeeded(level, opts);
|
|
224
|
+
}
|
|
225
|
+
onPlayFinished();
|
|
226
|
+
},
|
|
227
|
+
};
|
|
105
228
|
try {
|
|
106
229
|
const customPath = findCustomSound(level);
|
|
107
230
|
const platform = process.platform;
|
|
108
231
|
if (customPath) {
|
|
109
|
-
|
|
232
|
+
activeProcess = launchCustomSound(customPath, platform, handle);
|
|
110
233
|
}
|
|
111
234
|
else if (platform === "win32") {
|
|
112
|
-
|
|
235
|
+
activeProcess = launchSystemSoundWindows(level, handle);
|
|
113
236
|
}
|
|
114
237
|
else if (platform === "darwin") {
|
|
115
|
-
|
|
238
|
+
activeProcess = launchSystemSoundMacOS(level, handle);
|
|
116
239
|
}
|
|
117
240
|
else {
|
|
118
|
-
|
|
241
|
+
activeProcess = launchSystemSoundLinux(level, handle);
|
|
242
|
+
}
|
|
243
|
+
if (activeProcess != null) {
|
|
244
|
+
activeTimer = setTimeout(() => {
|
|
245
|
+
if (!handle.claim())
|
|
246
|
+
return;
|
|
247
|
+
log.warn(`通知音の再生がタイムアウトしました (${PLAY_TIMEOUT_MS}ms)`);
|
|
248
|
+
try {
|
|
249
|
+
activeProcess?.kill();
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
// ignore
|
|
253
|
+
}
|
|
254
|
+
handle.done(true);
|
|
255
|
+
}, PLAY_TIMEOUT_MS);
|
|
119
256
|
}
|
|
120
257
|
}
|
|
121
258
|
catch (err) {
|
|
259
|
+
if (!handle.claim())
|
|
260
|
+
return;
|
|
122
261
|
if (err instanceof Error) {
|
|
123
|
-
log.
|
|
262
|
+
log.warn(`通知音の再生に失敗しました: ${err.message}`);
|
|
263
|
+
}
|
|
264
|
+
handle.done(true);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* 通知音を再生する (fire-and-forget)。
|
|
269
|
+
* 同時再生は MAX_CONCURRENT 件に制限し、超えた場合はキューに積む。
|
|
270
|
+
* キューが MAX_QUEUE_SIZE を超えた場合は先頭 (最古) を破棄する。
|
|
271
|
+
* カスタム効果音ファイルがあればそちらを優先、なければ OS システムサウンドにフォールバック。
|
|
272
|
+
* 再生失敗はログに記録するのみで例外は投げない。
|
|
273
|
+
*/
|
|
274
|
+
function playSound(level, opts) {
|
|
275
|
+
if (disposed)
|
|
276
|
+
return;
|
|
277
|
+
if (activeCount < MAX_CONCURRENT) {
|
|
278
|
+
runPlay(level, opts);
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
if (playQueue.length >= MAX_QUEUE_SIZE) {
|
|
282
|
+
// critical は警報相当のため drop 候補から除外。それ以外の最古を落とす。
|
|
283
|
+
const dropIdx = playQueue.findIndex((e) => e.level !== "critical");
|
|
284
|
+
if (dropIdx >= 0) {
|
|
285
|
+
const [dropped] = playQueue.splice(dropIdx, 1);
|
|
286
|
+
log.debug(`通知音キューが上限 (${MAX_QUEUE_SIZE}) に達したため破棄しました: ${dropped.level}`);
|
|
287
|
+
}
|
|
288
|
+
else if (level !== "critical") {
|
|
289
|
+
// キューが critical で埋まっている → 非 critical の新規は諦める
|
|
290
|
+
log.debug(`通知音キューが critical で飽和しているため新規再生を破棄しました: ${level}`);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
// 新規も critical で、既存も全て critical → 最古の critical を落とす (従来動作)
|
|
295
|
+
const dropped = playQueue.shift();
|
|
296
|
+
log.debug(`通知音キューが上限 (${MAX_QUEUE_SIZE}) に達したため破棄しました: ${dropped?.level}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
playQueue.push({ level, isRetry: opts?.isRetry === true });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* 進行中の再生を停止し、キューをクリアする。
|
|
304
|
+
* アプリ終了時に呼ぶことでプロセスリークを防ぐ。
|
|
305
|
+
*/
|
|
306
|
+
function dispose() {
|
|
307
|
+
disposed = true;
|
|
308
|
+
// 世代を進めて旧 runPlay の遅延コールバックを無効化する
|
|
309
|
+
runGeneration++;
|
|
310
|
+
playQueue.length = 0;
|
|
311
|
+
if (activeTimer != null) {
|
|
312
|
+
clearTimeout(activeTimer);
|
|
313
|
+
activeTimer = null;
|
|
314
|
+
}
|
|
315
|
+
for (const timer of retryTimers) {
|
|
316
|
+
clearTimeout(timer);
|
|
317
|
+
}
|
|
318
|
+
retryTimers.clear();
|
|
319
|
+
if (activeProcess != null) {
|
|
320
|
+
try {
|
|
321
|
+
activeProcess.kill();
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
// ignore
|
|
124
325
|
}
|
|
326
|
+
activeProcess = null;
|
|
125
327
|
}
|
|
328
|
+
activeCount = 0;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* dispose 後に再利用できるよう内部状態をリセットする (テスト用)。
|
|
332
|
+
*/
|
|
333
|
+
function resetSoundPlayer() {
|
|
334
|
+
disposed = false;
|
|
335
|
+
// 世代を進めて旧 runPlay の遅延コールバックを無効化する
|
|
336
|
+
runGeneration++;
|
|
337
|
+
activeCount = 0;
|
|
338
|
+
playQueue.length = 0;
|
|
339
|
+
activeProcess = null;
|
|
340
|
+
if (activeTimer != null) {
|
|
341
|
+
clearTimeout(activeTimer);
|
|
342
|
+
activeTimer = null;
|
|
343
|
+
}
|
|
344
|
+
for (const timer of retryTimers) {
|
|
345
|
+
clearTimeout(timer);
|
|
346
|
+
}
|
|
347
|
+
retryTimers.clear();
|
|
126
348
|
}
|
|
127
349
|
// ── カスタム効果音再生 ──
|
|
128
|
-
function
|
|
350
|
+
function launchCustomSound(filePath, platform, onDone) {
|
|
129
351
|
if (platform === "win32") {
|
|
130
|
-
|
|
352
|
+
return launchCustomSoundWindows(filePath, onDone);
|
|
131
353
|
}
|
|
132
354
|
else if (platform === "darwin") {
|
|
133
|
-
|
|
355
|
+
return launchCustomSoundMacOS(filePath, onDone);
|
|
134
356
|
}
|
|
135
357
|
else {
|
|
136
|
-
|
|
358
|
+
return launchCustomSoundLinux(filePath, onDone);
|
|
137
359
|
}
|
|
138
360
|
}
|
|
139
361
|
/** Windows: winmm.dll mciSendString で mp3/wav を同期再生 */
|
|
140
|
-
function
|
|
362
|
+
function launchCustomSoundWindows(filePath, onDone) {
|
|
141
363
|
// MediaPlayer (WPF) は非同期ロードのためタイミング問題が起きやすい。
|
|
142
364
|
// mciSendString は同期 (wait) で確実に再生できる。
|
|
365
|
+
// execFile を使い cmd.exe のダブルクォーテーション解釈を回避する。
|
|
143
366
|
const escaped = filePath.replace(/'/g, "''");
|
|
144
367
|
const psCommand = [
|
|
145
|
-
`Add-Type -Namespace Win32 -Name Mci -MemberDefinition '[DllImport("winmm.dll",CharSet=
|
|
368
|
+
`Add-Type -Namespace Win32 -Name Mci -MemberDefinition '[DllImport("winmm.dll",CharSet=CharSet.Unicode)]public static extern int mciSendStringW(string cmd,System.Text.StringBuilder ret,int retLen,System.IntPtr hwnd);';`,
|
|
146
369
|
`[Win32.Mci]::mciSendStringW('open "' + '${escaped}' + '" type mpegvideo alias fleqsnd',$null,0,[IntPtr]::Zero)|Out-Null;`,
|
|
147
370
|
`[Win32.Mci]::mciSendStringW('play fleqsnd wait',$null,0,[IntPtr]::Zero)|Out-Null;`,
|
|
148
371
|
`[Win32.Mci]::mciSendStringW('close fleqsnd',$null,0,[IntPtr]::Zero)|Out-Null`,
|
|
149
372
|
].join(" ");
|
|
150
|
-
(0, child_process_1.
|
|
373
|
+
const proc = (0, child_process_1.execFile)("powershell", ["-NoProfile", "-Command", psCommand], (err) => {
|
|
374
|
+
if (!onDone.claim())
|
|
375
|
+
return;
|
|
151
376
|
if (err) {
|
|
152
|
-
log.
|
|
377
|
+
log.warn(`Windows カスタム通知音の再生に失敗しました: ${err.message}`);
|
|
378
|
+
onDone.done(true);
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
onDone.done();
|
|
153
382
|
}
|
|
154
383
|
});
|
|
384
|
+
return proc;
|
|
155
385
|
}
|
|
156
386
|
/** macOS: afplay で mp3/wav を再生 */
|
|
157
|
-
function
|
|
158
|
-
(0, child_process_1.execFile)("afplay", [filePath], (err) => {
|
|
387
|
+
function launchCustomSoundMacOS(filePath, onDone) {
|
|
388
|
+
const proc = (0, child_process_1.execFile)("afplay", [filePath], (err) => {
|
|
389
|
+
if (!onDone.claim())
|
|
390
|
+
return;
|
|
159
391
|
if (err) {
|
|
160
|
-
log.
|
|
392
|
+
log.warn(`macOS カスタム通知音の再生に失敗しました: ${err.message}`);
|
|
393
|
+
onDone.done(true);
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
onDone.done();
|
|
161
397
|
}
|
|
162
398
|
});
|
|
399
|
+
return proc;
|
|
163
400
|
}
|
|
164
401
|
/** Linux: ffplay → paplay → aplay のフォールバック */
|
|
165
|
-
function
|
|
402
|
+
function launchCustomSoundLinux(filePath, onDone) {
|
|
166
403
|
const ext = path.extname(filePath).toLowerCase();
|
|
167
404
|
if (ext === ".mp3") {
|
|
168
405
|
// mp3 は ffplay で再生
|
|
169
|
-
(0, child_process_1.execFile)("ffplay", ["-nodisp", "-autoexit", "-loglevel", "quiet", filePath], (err) => {
|
|
406
|
+
const proc = (0, child_process_1.execFile)("ffplay", ["-nodisp", "-autoexit", "-loglevel", "quiet", filePath], (err) => {
|
|
407
|
+
if (!onDone.claim())
|
|
408
|
+
return;
|
|
170
409
|
if (err) {
|
|
171
|
-
log.
|
|
410
|
+
log.warn(`Linux ffplay での再生に失敗しました: ${err.message}`);
|
|
172
411
|
printBell();
|
|
412
|
+
onDone.done(true);
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
onDone.done();
|
|
173
416
|
}
|
|
174
417
|
});
|
|
418
|
+
return proc;
|
|
175
419
|
}
|
|
176
420
|
else {
|
|
177
421
|
// wav は paplay → aplay のフォールバック
|
|
178
|
-
(0, child_process_1.execFile)("paplay", [filePath], (err) => {
|
|
422
|
+
const proc = (0, child_process_1.execFile)("paplay", [filePath], (err) => {
|
|
179
423
|
if (err) {
|
|
180
424
|
(0, child_process_1.execFile)("aplay", ["-q", filePath], (err2) => {
|
|
425
|
+
if (!onDone.claim())
|
|
426
|
+
return;
|
|
181
427
|
if (err2) {
|
|
182
|
-
log.
|
|
428
|
+
log.warn(`Linux 通知音の再生に失敗しました: ${err2.message}`);
|
|
183
429
|
printBell();
|
|
430
|
+
onDone.done(true);
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
onDone.done();
|
|
184
434
|
}
|
|
185
435
|
});
|
|
186
436
|
}
|
|
437
|
+
else {
|
|
438
|
+
if (!onDone.claim())
|
|
439
|
+
return;
|
|
440
|
+
onDone.done();
|
|
441
|
+
}
|
|
187
442
|
});
|
|
443
|
+
return proc;
|
|
188
444
|
}
|
|
189
445
|
}
|
|
190
446
|
// ── システムサウンドフォールバック ──
|
|
191
|
-
|
|
447
|
+
/** Windows システムサウンドの存在確認キャッシュ (null = 存在しない, undefined = 未検索) */
|
|
448
|
+
const windowsSoundCache = new Map();
|
|
449
|
+
/** Windows サウンドファイルの確認済みパスを返す。存在しなければ null。 */
|
|
450
|
+
function findWindowsSystemSound(level) {
|
|
451
|
+
if (windowsSoundCache.has(level)) {
|
|
452
|
+
return windowsSoundCache.get(level);
|
|
453
|
+
}
|
|
192
454
|
const soundFile = WINDOWS_SOUNDS[level];
|
|
193
455
|
const soundPath = path.join(process.env.SYSTEMROOT || "C:\\Windows", "Media", soundFile);
|
|
456
|
+
if (fs.existsSync(soundPath)) {
|
|
457
|
+
windowsSoundCache.set(level, soundPath);
|
|
458
|
+
return soundPath;
|
|
459
|
+
}
|
|
460
|
+
// フォールバック: Windows Default.wav
|
|
461
|
+
const defaultPath = path.join(process.env.SYSTEMROOT || "C:\\Windows", "Media", "Windows Default.wav");
|
|
462
|
+
if (fs.existsSync(defaultPath)) {
|
|
463
|
+
windowsSoundCache.set(level, defaultPath);
|
|
464
|
+
return defaultPath;
|
|
465
|
+
}
|
|
466
|
+
windowsSoundCache.set(level, null);
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
function launchSystemSoundWindows(level, onDone) {
|
|
470
|
+
const soundPath = findWindowsSystemSound(level);
|
|
471
|
+
if (soundPath == null) {
|
|
472
|
+
if (!onDone.claim())
|
|
473
|
+
return null;
|
|
474
|
+
log.warn(`Windows 通知音が見つかりません。bell にフォールバックします: ${WINDOWS_SOUNDS[level]}`);
|
|
475
|
+
printBell();
|
|
476
|
+
onDone.done(true);
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
194
479
|
const psCommand = `(New-Object System.Media.SoundPlayer '${soundPath}').PlaySync()`;
|
|
195
|
-
(0, child_process_1.
|
|
480
|
+
const proc = (0, child_process_1.execFile)("powershell", ["-NoProfile", "-Command", psCommand], (err) => {
|
|
481
|
+
if (!onDone.claim())
|
|
482
|
+
return;
|
|
196
483
|
if (err) {
|
|
197
|
-
log.
|
|
484
|
+
log.warn(`Windows 通知音の再生に失敗しました: ${err.message}`);
|
|
485
|
+
onDone.done(true);
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
onDone.done();
|
|
198
489
|
}
|
|
199
490
|
});
|
|
491
|
+
return proc;
|
|
200
492
|
}
|
|
201
|
-
function
|
|
493
|
+
function launchSystemSoundMacOS(level, onDone) {
|
|
202
494
|
const soundFile = MACOS_SOUNDS[level];
|
|
203
495
|
const soundPath = `/System/Library/Sounds/${soundFile}`;
|
|
204
|
-
(0, child_process_1.execFile)("afplay", [soundPath], (err) => {
|
|
496
|
+
const proc = (0, child_process_1.execFile)("afplay", [soundPath], (err) => {
|
|
497
|
+
if (!onDone.claim())
|
|
498
|
+
return;
|
|
205
499
|
if (err) {
|
|
206
|
-
log.
|
|
500
|
+
log.warn(`macOS 通知音の再生に失敗しました: ${err.message}`);
|
|
501
|
+
onDone.done(true);
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
onDone.done();
|
|
207
505
|
}
|
|
208
506
|
});
|
|
507
|
+
return proc;
|
|
209
508
|
}
|
|
210
|
-
function
|
|
509
|
+
function launchSystemSoundLinux(level, onDone) {
|
|
211
510
|
const eventName = LINUX_CANBERRA_EVENTS[level];
|
|
212
511
|
if (eventName === "bell") {
|
|
512
|
+
if (!onDone.claim())
|
|
513
|
+
return null;
|
|
213
514
|
printBell();
|
|
214
|
-
|
|
515
|
+
onDone.done();
|
|
516
|
+
return null;
|
|
215
517
|
}
|
|
216
|
-
(0, child_process_1.execFile)("canberra-gtk-play", ["-i", eventName], (err) => {
|
|
518
|
+
const proc = (0, child_process_1.execFile)("canberra-gtk-play", ["-i", eventName], (err) => {
|
|
519
|
+
if (!onDone.claim())
|
|
520
|
+
return;
|
|
217
521
|
if (err) {
|
|
218
|
-
log.
|
|
522
|
+
log.warn(`canberra-gtk-play 失敗、bell にフォールバック: ${err.message}`);
|
|
219
523
|
printBell();
|
|
524
|
+
onDone.done(true);
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
onDone.done();
|
|
220
528
|
}
|
|
221
529
|
});
|
|
530
|
+
return proc;
|
|
222
531
|
}
|
|
223
532
|
function printBell() {
|
|
224
533
|
try {
|
|
@@ -228,3 +537,56 @@ function printBell() {
|
|
|
228
537
|
// ignore
|
|
229
538
|
}
|
|
230
539
|
}
|
|
540
|
+
// ── バックエンド健康チェック ──
|
|
541
|
+
/** バックエンドプローブの timeout (ms) */
|
|
542
|
+
const BACKEND_PROBE_TIMEOUT_MS = 2_000;
|
|
543
|
+
/**
|
|
544
|
+
* 音声バックエンドが利用可能かを判定する。
|
|
545
|
+
* Linux: ffplay で 0.1 秒の無音サンプルを再生し、終了コードで判定する。
|
|
546
|
+
* PATH 上に ffplay があっても音声デバイスが使えない場合は ok=false を返す。
|
|
547
|
+
* Windows / macOS: 即座に ok=true を返す (ビルトインの再生経路が常に利用可能)。
|
|
548
|
+
*/
|
|
549
|
+
async function checkSoundBackend() {
|
|
550
|
+
const platform = process.platform;
|
|
551
|
+
if (platform === "win32")
|
|
552
|
+
return { ok: true, label: "winmm" };
|
|
553
|
+
if (platform === "darwin")
|
|
554
|
+
return { ok: true, label: "afplay" };
|
|
555
|
+
return await new Promise((resolve) => {
|
|
556
|
+
let settled = false;
|
|
557
|
+
let timer = null;
|
|
558
|
+
const proc = (0, child_process_1.execFile)("ffplay", ["-f", "lavfi", "-i", "anullsrc=d=0.1", "-nodisp", "-autoexit", "-loglevel", "quiet"], (err) => {
|
|
559
|
+
if (settled)
|
|
560
|
+
return;
|
|
561
|
+
settled = true;
|
|
562
|
+
if (timer != null)
|
|
563
|
+
clearTimeout(timer);
|
|
564
|
+
if (err) {
|
|
565
|
+
const e = err;
|
|
566
|
+
const reason = e.code === "ENOENT"
|
|
567
|
+
? "ffplay not found in PATH"
|
|
568
|
+
: `ffplay probe failed: ${e.message}`;
|
|
569
|
+
resolve({ ok: false, label: "ffplay", reason });
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
resolve({ ok: true, label: "ffplay" });
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
// モック環境などで execFile のコールバックが同期的に呼ばれ settled=true に
|
|
576
|
+
// なっている場合、ここで timer を作らないようガードする。
|
|
577
|
+
if (settled)
|
|
578
|
+
return;
|
|
579
|
+
timer = setTimeout(() => {
|
|
580
|
+
if (settled)
|
|
581
|
+
return;
|
|
582
|
+
settled = true;
|
|
583
|
+
try {
|
|
584
|
+
proc?.kill();
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
// ignore
|
|
588
|
+
}
|
|
589
|
+
resolve({ ok: false, label: "ffplay", reason: "probe timeout" });
|
|
590
|
+
}, BACKEND_PROBE_TIMEOUT_MS);
|
|
591
|
+
});
|
|
592
|
+
}
|