@pulso/companion 0.1.5 → 0.1.6
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/dist/index.js +106 -43
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -55,54 +55,101 @@ function runSwift(code, timeout = 1e4) {
|
|
|
55
55
|
child.stdin?.end();
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
|
-
var
|
|
59
|
-
var
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
var UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
59
|
+
var searchCache = /* @__PURE__ */ new Map();
|
|
60
|
+
var CACHE_TTL = 7 * 24 * 3600 * 1e3;
|
|
61
|
+
async function spotifySearch(query) {
|
|
62
|
+
const key = query.toLowerCase().trim();
|
|
63
|
+
const cached = searchCache.get(key);
|
|
64
|
+
if (cached && Date.now() - cached.ts < CACHE_TTL) {
|
|
65
|
+
return cached;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const res = await fetch(
|
|
69
|
+
`${API_URL}/tools/spotify/search?q=${encodeURIComponent(query)}`,
|
|
70
|
+
{ headers: { Authorization: `Bearer ${TOKEN}` } }
|
|
71
|
+
);
|
|
72
|
+
if (res.ok) {
|
|
73
|
+
const data = await res.json();
|
|
74
|
+
if (data.uri) {
|
|
75
|
+
searchCache.set(key, { ...data, ts: Date.now() });
|
|
76
|
+
return data;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
const trackIds = await searchBraveForSpotifyTracks(query);
|
|
82
|
+
if (trackIds.length === 0) return null;
|
|
83
|
+
const meta = await getTrackMetadata(trackIds[0]);
|
|
84
|
+
const result = {
|
|
85
|
+
uri: `spotify:track:${trackIds[0]}`,
|
|
86
|
+
name: meta?.name ?? query,
|
|
87
|
+
artist: meta?.artist ?? "Unknown"
|
|
88
|
+
};
|
|
89
|
+
searchCache.set(key, { ...result, ts: Date.now() });
|
|
90
|
+
pushToServerCache(query, result).catch(() => {
|
|
91
|
+
});
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
async function searchBraveForSpotifyTracks(query) {
|
|
62
95
|
try {
|
|
63
|
-
const
|
|
64
|
-
|
|
96
|
+
const searchQuery = `spotify track ${query} site:open.spotify.com`;
|
|
97
|
+
const url = `https://search.brave.com/search?q=${encodeURIComponent(searchQuery)}&source=web`;
|
|
98
|
+
const controller = new AbortController();
|
|
99
|
+
const timeout = setTimeout(() => controller.abort(), 8e3);
|
|
100
|
+
const res = await fetch(url, {
|
|
101
|
+
headers: { "User-Agent": UA, Accept: "text/html" },
|
|
102
|
+
signal: controller.signal
|
|
65
103
|
});
|
|
104
|
+
clearTimeout(timeout);
|
|
105
|
+
if (!res.ok) return [];
|
|
66
106
|
const html = await res.text();
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return
|
|
107
|
+
const trackIds = /* @__PURE__ */ new Set();
|
|
108
|
+
for (const m of html.matchAll(/https?:\/\/open\.spotify\.com\/track\/([a-zA-Z0-9]{22})/g)) {
|
|
109
|
+
trackIds.add(m[1]);
|
|
110
|
+
}
|
|
111
|
+
return [...trackIds];
|
|
72
112
|
} catch {
|
|
73
|
-
return
|
|
113
|
+
return [];
|
|
74
114
|
}
|
|
75
115
|
}
|
|
76
|
-
async function
|
|
77
|
-
const token = await getSpotifyToken();
|
|
78
|
-
if (!token) return null;
|
|
116
|
+
async function getTrackMetadata(trackId) {
|
|
79
117
|
try {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
const data = await res.json();
|
|
101
|
-
return data.tracks?.items?.[0]?.uri ?? null;
|
|
118
|
+
const controller = new AbortController();
|
|
119
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
120
|
+
const res = await fetch(`https://open.spotify.com/embed/track/${trackId}`, {
|
|
121
|
+
headers: { "User-Agent": UA, Accept: "text/html" },
|
|
122
|
+
signal: controller.signal
|
|
123
|
+
});
|
|
124
|
+
clearTimeout(timeout);
|
|
125
|
+
if (!res.ok) return null;
|
|
126
|
+
const html = await res.text();
|
|
127
|
+
const scriptMatch = html.match(/<script[^>]*>(\{"props":\{"pageProps".*?\})<\/script>/s);
|
|
128
|
+
if (!scriptMatch) return null;
|
|
129
|
+
const data = JSON.parse(scriptMatch[1]);
|
|
130
|
+
const entity = data?.props?.pageProps?.state?.data?.entity;
|
|
131
|
+
if (!entity?.name) return null;
|
|
132
|
+
return {
|
|
133
|
+
name: entity.name,
|
|
134
|
+
artist: entity.artists?.map((a) => a.name).join(", ") ?? "Unknown"
|
|
135
|
+
};
|
|
102
136
|
} catch {
|
|
103
137
|
return null;
|
|
104
138
|
}
|
|
105
139
|
}
|
|
140
|
+
async function pushToServerCache(query, result) {
|
|
141
|
+
try {
|
|
142
|
+
await fetch(`${API_URL}/tools/spotify/cache`, {
|
|
143
|
+
method: "POST",
|
|
144
|
+
headers: {
|
|
145
|
+
Authorization: `Bearer ${TOKEN}`,
|
|
146
|
+
"Content-Type": "application/json"
|
|
147
|
+
},
|
|
148
|
+
body: JSON.stringify({ query, ...result })
|
|
149
|
+
});
|
|
150
|
+
} catch {
|
|
151
|
+
}
|
|
152
|
+
}
|
|
106
153
|
async function handleCommand(command, params) {
|
|
107
154
|
try {
|
|
108
155
|
switch (command) {
|
|
@@ -170,20 +217,36 @@ async function handleCommand(command, params) {
|
|
|
170
217
|
case "search_play": {
|
|
171
218
|
const query = params.query;
|
|
172
219
|
if (!query) return { success: false, error: "Missing search query" };
|
|
173
|
-
const
|
|
174
|
-
if (
|
|
175
|
-
await runAppleScript(`tell application "Spotify" to play track "${
|
|
220
|
+
const result = await spotifySearch(query);
|
|
221
|
+
if (result) {
|
|
222
|
+
await runAppleScript(`tell application "Spotify" to play track "${result.uri}"`);
|
|
176
223
|
await new Promise((r) => setTimeout(r, 1500));
|
|
177
224
|
try {
|
|
178
225
|
const track = await runAppleScript('tell application "Spotify" to name of current track');
|
|
179
226
|
const artist = await runAppleScript('tell application "Spotify" to artist of current track');
|
|
180
|
-
|
|
227
|
+
const state = await runAppleScript('tell application "Spotify" to player state as string');
|
|
228
|
+
return {
|
|
229
|
+
success: true,
|
|
230
|
+
data: {
|
|
231
|
+
searched: query,
|
|
232
|
+
resolved: `${result.name} - ${result.artist}`,
|
|
233
|
+
nowPlaying: `${track} - ${artist}`,
|
|
234
|
+
state
|
|
235
|
+
}
|
|
236
|
+
};
|
|
181
237
|
} catch {
|
|
182
|
-
return { success: true, data: { searched: query, note: "Playing track" } };
|
|
238
|
+
return { success: true, data: { searched: query, resolved: `${result.name} - ${result.artist}`, note: "Playing track" } };
|
|
183
239
|
}
|
|
184
240
|
}
|
|
185
241
|
await runShell(`open "spotify:search:${encodeURIComponent(query)}"`);
|
|
186
|
-
return {
|
|
242
|
+
return {
|
|
243
|
+
success: true,
|
|
244
|
+
data: {
|
|
245
|
+
searched: query,
|
|
246
|
+
note: "Search engine unavailable. Opened Spotify search \u2014 use computer-use (screenshot + click) to select the correct song.",
|
|
247
|
+
requiresComputerUse: true
|
|
248
|
+
}
|
|
249
|
+
};
|
|
187
250
|
}
|
|
188
251
|
case "volume": {
|
|
189
252
|
const level = params.level;
|