opencode-plugin-boops 2.5.3 → 2.6.2
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/cli/browse +140 -45
- package/index.ts +20 -20
- package/package.json +1 -1
package/cli/browse
CHANGED
|
@@ -22,17 +22,43 @@ const sounds = JSON.parse(readFileSync(soundsPath, "utf-8"));
|
|
|
22
22
|
|
|
23
23
|
// Available OpenCode events
|
|
24
24
|
const availableEvents = [
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
25
|
+
"command.executed",
|
|
26
|
+
"file.edited",
|
|
27
|
+
"file.watcher.updated",
|
|
28
|
+
"installation.updated",
|
|
29
|
+
"lsp.client.diagnostics",
|
|
30
|
+
"lsp.updated",
|
|
31
|
+
"message.part.removed",
|
|
32
|
+
"message.part.updated",
|
|
33
|
+
"message.removed",
|
|
34
|
+
"message.updated",
|
|
35
|
+
"permission.asked",
|
|
36
|
+
"permission.replied",
|
|
37
|
+
"server.connected",
|
|
38
|
+
"session.compacted",
|
|
39
|
+
"session.created",
|
|
40
|
+
"session.deleted",
|
|
41
|
+
"session.diff",
|
|
42
|
+
"session.error",
|
|
43
|
+
"session.idle",
|
|
44
|
+
"session.status",
|
|
45
|
+
"session.updated",
|
|
31
46
|
"todo.updated",
|
|
32
|
-
"tool.execute.
|
|
33
|
-
"
|
|
47
|
+
"tool.execute.after",
|
|
48
|
+
"tool.execute.before",
|
|
49
|
+
"tui.command.execute",
|
|
50
|
+
"tui.prompt.append",
|
|
51
|
+
"tui.toast.show"
|
|
34
52
|
];
|
|
35
53
|
|
|
54
|
+
// System/custom events (separate section)
|
|
55
|
+
const systemEvents = [
|
|
56
|
+
"__fallback"
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
// Combined array for the picker (system events at the end)
|
|
60
|
+
const allEvents = [...availableEvents, ...systemEvents];
|
|
61
|
+
|
|
36
62
|
// Cache directory
|
|
37
63
|
const CACHE_DIR = "/tmp/opencode-boops/cache";
|
|
38
64
|
|
|
@@ -59,9 +85,6 @@ const PLUGIN_INSTALLED = isPluginInstalled();
|
|
|
59
85
|
|
|
60
86
|
// Event descriptions for the picker
|
|
61
87
|
const eventDescriptions = {
|
|
62
|
-
"session.idle": "AI completes response",
|
|
63
|
-
"permission.asked": "AI needs permission",
|
|
64
|
-
"session.error": "Error occurs",
|
|
65
88
|
"command.executed": "Command executed",
|
|
66
89
|
"file.edited": "File edited",
|
|
67
90
|
"file.watcher.updated": "File watcher detected change",
|
|
@@ -72,20 +95,24 @@ const eventDescriptions = {
|
|
|
72
95
|
"message.part.updated": "Message part updated",
|
|
73
96
|
"message.removed": "Message removed",
|
|
74
97
|
"message.updated": "Message updated",
|
|
98
|
+
"permission.asked": "AI needs permission",
|
|
75
99
|
"permission.replied": "Permission response given",
|
|
76
100
|
"server.connected": "Connected to server",
|
|
77
|
-
"session.created": "New session created",
|
|
78
101
|
"session.compacted": "Session compacted",
|
|
102
|
+
"session.created": "New session created",
|
|
79
103
|
"session.deleted": "Session deleted",
|
|
80
104
|
"session.diff": "Session diff generated",
|
|
105
|
+
"session.error": "Error occurs",
|
|
106
|
+
"session.idle": "AI completes response",
|
|
81
107
|
"session.status": "Session status changed",
|
|
82
108
|
"session.updated": "Session updated",
|
|
83
109
|
"todo.updated": "Todo list updated",
|
|
84
|
-
"tool.execute.before": "Before tool execution",
|
|
85
110
|
"tool.execute.after": "After tool execution",
|
|
86
|
-
"
|
|
111
|
+
"tool.execute.before": "Before tool execution",
|
|
87
112
|
"tui.command.execute": "TUI command executed",
|
|
88
|
-
"tui.
|
|
113
|
+
"tui.prompt.append": "Text appended to prompt",
|
|
114
|
+
"tui.toast.show": "Toast notification shown",
|
|
115
|
+
"__fallback": "Fallback sound when primary fails"
|
|
89
116
|
};
|
|
90
117
|
|
|
91
118
|
// Load current boops.toml config
|
|
@@ -124,6 +151,14 @@ function saveConfig(config) {
|
|
|
124
151
|
}
|
|
125
152
|
}
|
|
126
153
|
|
|
154
|
+
// Add fallbacks section if there are any
|
|
155
|
+
if (config.fallbacks && Object.keys(config.fallbacks).length > 0) {
|
|
156
|
+
content += "\n[fallbacks]\n";
|
|
157
|
+
for (const [key, sound] of Object.entries(config.fallbacks).sort()) {
|
|
158
|
+
content += `${key} = "${sound}"\n`;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
127
162
|
writeFileSync(configPath, content);
|
|
128
163
|
return true;
|
|
129
164
|
}
|
|
@@ -810,9 +845,9 @@ async function browse() {
|
|
|
810
845
|
|
|
811
846
|
// Instructions
|
|
812
847
|
if (PLUGIN_INSTALLED) {
|
|
813
|
-
console.log(' \x1b[38;5;240m
|
|
848
|
+
console.log(' \x1b[38;5;240m\x1b[38;5;67m↑/↓\x1b[38;5;240m navigate \x1b[38;5;67mEnter\x1b[38;5;240m play current \x1b[38;5;67mCtrl+S\x1b[38;5;240m save & exit \x1b[38;5;67mESC\x1b[38;5;240m cancel\x1b[0m');
|
|
814
849
|
} else {
|
|
815
|
-
console.log(' \x1b[38;5;240m
|
|
850
|
+
console.log(' \x1b[38;5;240m\x1b[38;5;67m↑/↓\x1b[38;5;240m navigate \x1b[38;5;67mEnter\x1b[38;5;240m play current \x1b[38;5;67mESC\x1b[38;5;240m cancel \x1b[38;5;208m(save disabled - plugin not installed)\x1b[0m');
|
|
816
851
|
}
|
|
817
852
|
console.log('');
|
|
818
853
|
|
|
@@ -821,13 +856,25 @@ async function browse() {
|
|
|
821
856
|
// Fixed lines: 4 (top) + 2 (bottom) = 6
|
|
822
857
|
const fixedLines = 6;
|
|
823
858
|
const maxVisibleEvents = Math.max(1, termHeight - fixedLines);
|
|
824
|
-
const scrollOffset = Math.max(0, Math.min(state.pickerSelectedEvent - Math.floor(maxVisibleEvents / 2),
|
|
825
|
-
const endIndex = Math.min(scrollOffset + maxVisibleEvents,
|
|
859
|
+
const scrollOffset = Math.max(0, Math.min(state.pickerSelectedEvent - Math.floor(maxVisibleEvents / 2), allEvents.length - maxVisibleEvents));
|
|
860
|
+
const endIndex = Math.min(scrollOffset + maxVisibleEvents, allEvents.length);
|
|
826
861
|
|
|
827
862
|
for (let i = scrollOffset; i < endIndex; i++) {
|
|
828
|
-
const event =
|
|
863
|
+
const event = allEvents[i];
|
|
829
864
|
const isSelected = i === state.pickerSelectedEvent;
|
|
830
|
-
|
|
865
|
+
|
|
866
|
+
// Add separator before system events
|
|
867
|
+
if (i === availableEvents.length && i >= scrollOffset) {
|
|
868
|
+
console.log(` ${'─'.repeat(Math.min(60, termWidth - 4))}`);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Get current sound from appropriate config section
|
|
872
|
+
let currentSound = null;
|
|
873
|
+
if (event === '__fallback') {
|
|
874
|
+
currentSound = config.fallbacks ? config.fallbacks.default : null;
|
|
875
|
+
} else {
|
|
876
|
+
currentSound = config.sounds ? config.sounds[event] : null;
|
|
877
|
+
}
|
|
831
878
|
|
|
832
879
|
const selectionBg = isSelected ? '\x1b[48;5;235m' : '';
|
|
833
880
|
const reset = '\x1b[0m';
|
|
@@ -1085,15 +1132,15 @@ async function browse() {
|
|
|
1085
1132
|
const config = loadCurrentConfig();
|
|
1086
1133
|
const boxX = termWidth - pickerWidth;
|
|
1087
1134
|
const boxStartY = stickyNavbar ? (topBlankLine + 4) : (topBlankLine + 1); // Start below header in sticky mode, else near top
|
|
1088
|
-
const boxHeight = Math.min(
|
|
1135
|
+
const boxHeight = Math.min(allEvents.length + 5, termHeight - boxStartY - fixedFooterLines); // +5 for separator line
|
|
1089
1136
|
|
|
1090
1137
|
// Calculate scroll offset for event list
|
|
1091
1138
|
const maxVisibleEvents = boxHeight - 6; // Reserve space for header(2) + description(2) + footer(2)
|
|
1092
|
-
const eventScrollOffset = Math.max(0, Math.min(state.pickerSelectedEvent - Math.floor(maxVisibleEvents / 2),
|
|
1093
|
-
const eventEndIndex = Math.min(eventScrollOffset + maxVisibleEvents,
|
|
1139
|
+
const eventScrollOffset = Math.max(0, Math.min(state.pickerSelectedEvent - Math.floor(maxVisibleEvents / 2), allEvents.length - maxVisibleEvents));
|
|
1140
|
+
const eventEndIndex = Math.min(eventScrollOffset + maxVisibleEvents, allEvents.length);
|
|
1094
1141
|
|
|
1095
1142
|
// Get selected event info
|
|
1096
|
-
const selectedEvent =
|
|
1143
|
+
const selectedEvent = allEvents[state.pickerSelectedEvent];
|
|
1097
1144
|
const eventDesc = eventDescriptions[selectedEvent] || "No description available";
|
|
1098
1145
|
|
|
1099
1146
|
// Draw each line of the picker box
|
|
@@ -1101,6 +1148,11 @@ async function browse() {
|
|
|
1101
1148
|
const screenY = boxStartY + row;
|
|
1102
1149
|
process.stdout.write(`\x1b[${screenY};${boxX}H`); // Position cursor
|
|
1103
1150
|
|
|
1151
|
+
// Clear the area where we'll draw the picker (from cursor to end of line)
|
|
1152
|
+
// Draw a solid background to cover anything underneath
|
|
1153
|
+
process.stdout.write(' '.repeat(pickerWidth));
|
|
1154
|
+
process.stdout.write(`\x1b[${screenY};${boxX}H`); // Reposition cursor
|
|
1155
|
+
|
|
1104
1156
|
if (row === 0) {
|
|
1105
1157
|
// Top border with title
|
|
1106
1158
|
const title = ` Assign to Event `;
|
|
@@ -1132,10 +1184,23 @@ async function browse() {
|
|
|
1132
1184
|
} else {
|
|
1133
1185
|
// Event list item
|
|
1134
1186
|
const eventIndex = eventScrollOffset + (row - 3);
|
|
1187
|
+
|
|
1135
1188
|
if (eventIndex < eventEndIndex) {
|
|
1136
|
-
const event =
|
|
1189
|
+
const event = allEvents[eventIndex];
|
|
1137
1190
|
const isSelected = eventIndex === state.pickerSelectedEvent;
|
|
1138
|
-
|
|
1191
|
+
|
|
1192
|
+
// Show separator before system events
|
|
1193
|
+
if (eventIndex === availableEvents.length) {
|
|
1194
|
+
process.stdout.write(`\x1b[38;5;240m│ ${'─'.repeat(pickerWidth - 4)} │\x1b[0m`);
|
|
1195
|
+
} else {
|
|
1196
|
+
|
|
1197
|
+
// Get current sound from appropriate config section
|
|
1198
|
+
let currentSound = null;
|
|
1199
|
+
if (event === '__fallback') {
|
|
1200
|
+
currentSound = config.fallbacks ? config.fallbacks.default : null;
|
|
1201
|
+
} else {
|
|
1202
|
+
currentSound = config.sounds ? config.sounds[event] : null;
|
|
1203
|
+
}
|
|
1139
1204
|
|
|
1140
1205
|
const bg = isSelected ? '\x1b[48;5;235m' : '';
|
|
1141
1206
|
|
|
@@ -1153,10 +1218,11 @@ async function browse() {
|
|
|
1153
1218
|
: event.padEnd(pickerWidth - 6);
|
|
1154
1219
|
}
|
|
1155
1220
|
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1221
|
+
const indicator = currentSound ? '•' : ' ';
|
|
1222
|
+
const indicatorColor = currentSound ? '\x1b[38;5;76m' : '';
|
|
1223
|
+
|
|
1224
|
+
process.stdout.write(`\x1b[38;5;240m│\x1b[0m${bg}${indicatorColor}${indicator}\x1b[0m${bg} ${eventText} \x1b[0m\x1b[38;5;240m│\x1b[0m`);
|
|
1225
|
+
}
|
|
1160
1226
|
} else {
|
|
1161
1227
|
// Empty line
|
|
1162
1228
|
process.stdout.write(`\x1b[38;5;240m│${' '.repeat(pickerWidth - 2)}│\x1b[0m`);
|
|
@@ -1176,7 +1242,7 @@ async function browse() {
|
|
|
1176
1242
|
if (state.searchQuery || state.searchMode) {
|
|
1177
1243
|
// Search mode: show search box on left, hints on right
|
|
1178
1244
|
const searchBox = ` /${state.searchQuery}`;
|
|
1179
|
-
const searchHint = '\x1b[38;5;240m
|
|
1245
|
+
const searchHint = '\x1b[38;5;240m\x1b[38;5;67m/\x1b[38;5;240m to refine \x1b[38;5;67mesc\x1b[38;5;240m to clear\x1b[0m';
|
|
1180
1246
|
const searchBoxVisible = 2 + state.searchQuery.length; // " /" + query
|
|
1181
1247
|
const hintVisible = getVisibleLength(searchHint);
|
|
1182
1248
|
|
|
@@ -1216,10 +1282,10 @@ async function browse() {
|
|
|
1216
1282
|
let controls;
|
|
1217
1283
|
if (state.pickerMode) {
|
|
1218
1284
|
controls = PLUGIN_INSTALLED
|
|
1219
|
-
? ' \x1b[38;5;240m
|
|
1220
|
-
: ' \x1b[38;5;240m
|
|
1285
|
+
? ' \x1b[38;5;240m\x1b[38;5;67m↑/↓\x1b[38;5;240m select \x1b[38;5;67menter\x1b[38;5;240m play \x1b[38;5;67ma\x1b[38;5;240m assign \x1b[38;5;67md\x1b[38;5;240m unassign \x1b[38;5;67m←/esc\x1b[38;5;240m close\x1b[0m'
|
|
1286
|
+
: ' \x1b[38;5;240m\x1b[38;5;67m↑/↓\x1b[38;5;240m select event \x1b[38;5;67menter\x1b[38;5;240m play \x1b[38;5;67m←/esc\x1b[38;5;240m close \x1b[38;5;208m(save disabled)\x1b[0m';
|
|
1221
1287
|
} else {
|
|
1222
|
-
controls = ' \x1b[38;5;240m
|
|
1288
|
+
controls = ' \x1b[38;5;240m\x1b[38;5;67m↑/↓\x1b[38;5;240m navigate \x1b[38;5;67m←/→\x1b[38;5;240m tags \x1b[38;5;67m/\x1b[38;5;240m search \x1b[38;5;67ms\x1b[38;5;240m assign \x1b[38;5;67menter\x1b[38;5;240m play \x1b[38;5;67mq\x1b[38;5;240m quit\x1b[0m';
|
|
1223
1289
|
}
|
|
1224
1290
|
|
|
1225
1291
|
// Attribution with manual hover/click
|
|
@@ -1845,7 +1911,7 @@ async function browse() {
|
|
|
1845
1911
|
return;
|
|
1846
1912
|
} else if (key === '\u001b[B' || key === 'j') {
|
|
1847
1913
|
// Down arrow
|
|
1848
|
-
if (state.pickerSelectedEvent <
|
|
1914
|
+
if (state.pickerSelectedEvent < allEvents.length - 1) {
|
|
1849
1915
|
state.pickerSelectedEvent++;
|
|
1850
1916
|
}
|
|
1851
1917
|
render();
|
|
@@ -1853,8 +1919,15 @@ async function browse() {
|
|
|
1853
1919
|
} else if (key === '\r' || key === ' ') {
|
|
1854
1920
|
// Enter/Space - play current sound for selected event
|
|
1855
1921
|
const config = loadCurrentConfig();
|
|
1856
|
-
const event =
|
|
1857
|
-
|
|
1922
|
+
const event = allEvents[state.pickerSelectedEvent];
|
|
1923
|
+
|
|
1924
|
+
// Get current sound from appropriate config section
|
|
1925
|
+
let currentSound = null;
|
|
1926
|
+
if (event === '__fallback') {
|
|
1927
|
+
currentSound = config.fallbacks ? config.fallbacks.default : null;
|
|
1928
|
+
} else {
|
|
1929
|
+
currentSound = config.sounds ? config.sounds[event] : null;
|
|
1930
|
+
}
|
|
1858
1931
|
if (currentSound) {
|
|
1859
1932
|
const soundEntry = sounds.find(s => s.id === currentSound || s.name === currentSound);
|
|
1860
1933
|
if (soundEntry) {
|
|
@@ -1870,10 +1943,16 @@ async function browse() {
|
|
|
1870
1943
|
} else if (key === 'a') {
|
|
1871
1944
|
// 'a' - assign sound to selected event (auto-saves)
|
|
1872
1945
|
const config = loadCurrentConfig();
|
|
1873
|
-
const event =
|
|
1946
|
+
const event = allEvents[state.pickerSelectedEvent];
|
|
1874
1947
|
|
|
1875
|
-
if
|
|
1876
|
-
|
|
1948
|
+
// Check if this is a system event
|
|
1949
|
+
if (event === '__fallback') {
|
|
1950
|
+
if (!config.fallbacks) config.fallbacks = {};
|
|
1951
|
+
config.fallbacks.default = state.pickerSound.name; // Save friendly name
|
|
1952
|
+
} else {
|
|
1953
|
+
if (!config.sounds) config.sounds = {};
|
|
1954
|
+
config.sounds[event] = state.pickerSound.name; // Save friendly name, not ID
|
|
1955
|
+
}
|
|
1877
1956
|
|
|
1878
1957
|
// Auto-save to config file
|
|
1879
1958
|
saveConfig(config);
|
|
@@ -1884,11 +1963,19 @@ async function browse() {
|
|
|
1884
1963
|
} else if (key === 'd') {
|
|
1885
1964
|
// 'd' - unassign/delete sound from selected event
|
|
1886
1965
|
const config = loadCurrentConfig();
|
|
1887
|
-
const event =
|
|
1966
|
+
const event = allEvents[state.pickerSelectedEvent];
|
|
1888
1967
|
|
|
1889
|
-
if
|
|
1890
|
-
|
|
1891
|
-
|
|
1968
|
+
// Check if this is a system event
|
|
1969
|
+
if (event === '__fallback') {
|
|
1970
|
+
if (config.fallbacks && config.fallbacks.default) {
|
|
1971
|
+
delete config.fallbacks.default;
|
|
1972
|
+
saveConfig(config);
|
|
1973
|
+
}
|
|
1974
|
+
} else {
|
|
1975
|
+
if (config.sounds && config.sounds[event]) {
|
|
1976
|
+
delete config.sounds[event];
|
|
1977
|
+
saveConfig(config);
|
|
1978
|
+
}
|
|
1892
1979
|
}
|
|
1893
1980
|
|
|
1894
1981
|
// Stay in picker mode
|
|
@@ -1985,6 +2072,7 @@ async function browse() {
|
|
|
1985
2072
|
|
|
1986
2073
|
case '\u001b[A': // Up arrow
|
|
1987
2074
|
case 'k':
|
|
2075
|
+
if (state.pickerMode) break; // Picker handles its own navigation
|
|
1988
2076
|
if (state.selectedLine > 0) {
|
|
1989
2077
|
state.selectedLine--;
|
|
1990
2078
|
// Skip non-interactive lines
|
|
@@ -2026,6 +2114,7 @@ async function browse() {
|
|
|
2026
2114
|
|
|
2027
2115
|
case '\u001b[B': // Down arrow
|
|
2028
2116
|
case 'j':
|
|
2117
|
+
if (state.pickerMode) break; // Picker handles its own navigation
|
|
2029
2118
|
if (state.selectedLine < lines.length - 1) {
|
|
2030
2119
|
state.selectedLine++;
|
|
2031
2120
|
// Skip non-interactive lines
|
|
@@ -2050,6 +2139,7 @@ async function browse() {
|
|
|
2050
2139
|
|
|
2051
2140
|
case '\u001b[C': // Right arrow
|
|
2052
2141
|
case 'l':
|
|
2142
|
+
if (state.pickerMode) break; // Picker doesn't use right arrow
|
|
2053
2143
|
// Navigate tags right, maintaining sound position
|
|
2054
2144
|
if (state.selectedTagIndex < allTags.length - 1) {
|
|
2055
2145
|
state.selectedTagIndex++;
|
|
@@ -2072,6 +2162,8 @@ async function browse() {
|
|
|
2072
2162
|
|
|
2073
2163
|
case '\u001b[D': // Left arrow
|
|
2074
2164
|
case 'h':
|
|
2165
|
+
// Picker mode: left arrow exits (handled earlier in picker section)
|
|
2166
|
+
if (state.pickerMode) break; // Already handled in picker mode section
|
|
2075
2167
|
// Navigate tags left, maintaining sound position
|
|
2076
2168
|
if (state.selectedTagIndex > -1) {
|
|
2077
2169
|
state.selectedTagIndex--;
|
|
@@ -2093,6 +2185,7 @@ async function browse() {
|
|
|
2093
2185
|
break;
|
|
2094
2186
|
|
|
2095
2187
|
case '/':
|
|
2188
|
+
if (state.pickerMode) break; // Block search when picker is open
|
|
2096
2189
|
// Enter search mode (or re-enter if results are showing)
|
|
2097
2190
|
state.searchMode = true;
|
|
2098
2191
|
if (!state.searchQuery) {
|
|
@@ -2104,6 +2197,7 @@ async function browse() {
|
|
|
2104
2197
|
break;
|
|
2105
2198
|
|
|
2106
2199
|
case 's':
|
|
2200
|
+
if (state.pickerMode) break; // Block opening another picker
|
|
2107
2201
|
// Open event picker for selected sound
|
|
2108
2202
|
{
|
|
2109
2203
|
const selectedLine = lines[state.selectedLine];
|
|
@@ -2119,6 +2213,7 @@ async function browse() {
|
|
|
2119
2213
|
case 'p':
|
|
2120
2214
|
case '\r': // Enter key
|
|
2121
2215
|
case ' ': // Space key
|
|
2216
|
+
if (state.pickerMode) break; // Picker handles these keys
|
|
2122
2217
|
{
|
|
2123
2218
|
const selectedLine = lines[state.selectedLine];
|
|
2124
2219
|
|
package/index.ts
CHANGED
|
@@ -171,35 +171,35 @@ export const BoopsPlugin: Plugin = async ({ client }) => {
|
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
// Get cache path for
|
|
175
|
-
const getCachePath = (
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
174
|
+
// Get cache path for a sound (uses URL hash to ensure uniqueness)
|
|
175
|
+
const getCachePath = (url: string): string => {
|
|
176
|
+
// Use a hash of the URL to ensure unique cache keys
|
|
177
|
+
// This works for all URLs (notificationsounds.com, custom URLs, etc.)
|
|
178
|
+
const hash = Bun.hash(url).toString(16)
|
|
179
|
+
|
|
180
|
+
// Try to preserve the file extension for better debugging
|
|
181
|
+
let ext = '.ogg'
|
|
182
|
+
try {
|
|
183
|
+
const urlObj = new URL(url)
|
|
184
|
+
const pathname = urlObj.pathname
|
|
185
|
+
const extMatch = pathname.match(/\.(\w+)$/)
|
|
186
|
+
if (extMatch) ext = `.${extMatch[1]}`
|
|
187
|
+
} catch {
|
|
188
|
+
// Use default extension if URL parsing fails
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return join(cacheDir, `${hash}${ext}`)
|
|
179
192
|
}
|
|
180
193
|
|
|
181
|
-
// Download and cache a sound file from URL
|
|
194
|
+
// Download and cache a sound file from URL
|
|
182
195
|
const downloadSound = async (url: string, eventName: string): Promise<string> => {
|
|
183
|
-
const cachedPath = getCachePath(
|
|
196
|
+
const cachedPath = getCachePath(url)
|
|
184
197
|
|
|
185
198
|
// Return cached file if it exists
|
|
186
199
|
if (existsSync(cachedPath)) {
|
|
187
200
|
return cachedPath
|
|
188
201
|
}
|
|
189
202
|
|
|
190
|
-
// Delete any old cached file for this event (in case URL changed)
|
|
191
|
-
try {
|
|
192
|
-
const files = await readdir(cacheDir)
|
|
193
|
-
const safeName = eventName.replace(/\./g, "-")
|
|
194
|
-
for (const file of files) {
|
|
195
|
-
if (file.startsWith(safeName)) {
|
|
196
|
-
await unlink(join(cacheDir, file))
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
} catch {
|
|
200
|
-
// Ignore errors
|
|
201
|
-
}
|
|
202
|
-
|
|
203
203
|
// Download the file
|
|
204
204
|
await log(`Downloading sound for ${eventName}: ${url}`)
|
|
205
205
|
try {
|