@pulso/companion 0.1.5 → 0.1.7
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 +134 -42
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -55,54 +55,130 @@ 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
|
+
}
|
|
62
67
|
try {
|
|
63
|
-
const res = await fetch(
|
|
64
|
-
|
|
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
|
+
let trackIds = await searchBraveForSpotifyTracks(query);
|
|
82
|
+
if (trackIds.length === 0) {
|
|
83
|
+
trackIds = await searchStartpageForSpotifyTracks(query);
|
|
84
|
+
}
|
|
85
|
+
if (trackIds.length === 0) return null;
|
|
86
|
+
const meta = await getTrackMetadata(trackIds[0]);
|
|
87
|
+
const result = {
|
|
88
|
+
uri: `spotify:track:${trackIds[0]}`,
|
|
89
|
+
name: meta?.name ?? query,
|
|
90
|
+
artist: meta?.artist ?? "Unknown"
|
|
91
|
+
};
|
|
92
|
+
searchCache.set(key, { ...result, ts: Date.now() });
|
|
93
|
+
pushToServerCache(query, result).catch(() => {
|
|
94
|
+
});
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
async function searchBraveForSpotifyTracks(query) {
|
|
98
|
+
try {
|
|
99
|
+
const searchQuery = `spotify track ${query} site:open.spotify.com`;
|
|
100
|
+
const url = `https://search.brave.com/search?q=${encodeURIComponent(searchQuery)}&source=web`;
|
|
101
|
+
const controller = new AbortController();
|
|
102
|
+
const timeout = setTimeout(() => controller.abort(), 8e3);
|
|
103
|
+
const res = await fetch(url, {
|
|
104
|
+
headers: { "User-Agent": UA, Accept: "text/html" },
|
|
105
|
+
signal: controller.signal
|
|
65
106
|
});
|
|
107
|
+
clearTimeout(timeout);
|
|
108
|
+
if (!res.ok) return [];
|
|
66
109
|
const html = await res.text();
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return
|
|
110
|
+
const trackIds = /* @__PURE__ */ new Set();
|
|
111
|
+
for (const m of html.matchAll(/https?:\/\/open\.spotify\.com\/track\/([a-zA-Z0-9]{22})/g)) {
|
|
112
|
+
trackIds.add(m[1]);
|
|
113
|
+
}
|
|
114
|
+
return [...trackIds];
|
|
72
115
|
} catch {
|
|
73
|
-
return
|
|
116
|
+
return [];
|
|
74
117
|
}
|
|
75
118
|
}
|
|
76
|
-
async function
|
|
77
|
-
const token = await getSpotifyToken();
|
|
78
|
-
if (!token) return null;
|
|
119
|
+
async function searchStartpageForSpotifyTracks(query) {
|
|
79
120
|
try {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return null;
|
|
121
|
+
const searchQuery = `${query} spotify open.spotify.com`;
|
|
122
|
+
const controller = new AbortController();
|
|
123
|
+
const timeout = setTimeout(() => controller.abort(), 8e3);
|
|
124
|
+
const res = await fetch("https://www.startpage.com/sp/search", {
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: {
|
|
127
|
+
"User-Agent": UA,
|
|
128
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
129
|
+
},
|
|
130
|
+
body: `query=${encodeURIComponent(searchQuery)}`,
|
|
131
|
+
signal: controller.signal
|
|
132
|
+
});
|
|
133
|
+
clearTimeout(timeout);
|
|
134
|
+
if (!res.ok) return [];
|
|
135
|
+
const html = await res.text();
|
|
136
|
+
const trackIds = /* @__PURE__ */ new Set();
|
|
137
|
+
for (const m of html.matchAll(/open\.spotify\.com(?:\/intl-[a-z]+)?\/track\/([a-zA-Z0-9]{22})/g)) {
|
|
138
|
+
trackIds.add(m[1]);
|
|
99
139
|
}
|
|
100
|
-
|
|
101
|
-
|
|
140
|
+
return [...trackIds];
|
|
141
|
+
} catch {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function getTrackMetadata(trackId) {
|
|
146
|
+
try {
|
|
147
|
+
const controller = new AbortController();
|
|
148
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
149
|
+
const res = await fetch(`https://open.spotify.com/embed/track/${trackId}`, {
|
|
150
|
+
headers: { "User-Agent": UA, Accept: "text/html" },
|
|
151
|
+
signal: controller.signal
|
|
152
|
+
});
|
|
153
|
+
clearTimeout(timeout);
|
|
154
|
+
if (!res.ok) return null;
|
|
155
|
+
const html = await res.text();
|
|
156
|
+
const scriptMatch = html.match(/<script[^>]*>(\{"props":\{"pageProps".*?\})<\/script>/s);
|
|
157
|
+
if (!scriptMatch) return null;
|
|
158
|
+
const data = JSON.parse(scriptMatch[1]);
|
|
159
|
+
const entity = data?.props?.pageProps?.state?.data?.entity;
|
|
160
|
+
if (!entity?.name) return null;
|
|
161
|
+
return {
|
|
162
|
+
name: entity.name,
|
|
163
|
+
artist: entity.artists?.map((a) => a.name).join(", ") ?? "Unknown"
|
|
164
|
+
};
|
|
102
165
|
} catch {
|
|
103
166
|
return null;
|
|
104
167
|
}
|
|
105
168
|
}
|
|
169
|
+
async function pushToServerCache(query, result) {
|
|
170
|
+
try {
|
|
171
|
+
await fetch(`${API_URL}/tools/spotify/cache`, {
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers: {
|
|
174
|
+
Authorization: `Bearer ${TOKEN}`,
|
|
175
|
+
"Content-Type": "application/json"
|
|
176
|
+
},
|
|
177
|
+
body: JSON.stringify({ query, ...result })
|
|
178
|
+
});
|
|
179
|
+
} catch {
|
|
180
|
+
}
|
|
181
|
+
}
|
|
106
182
|
async function handleCommand(command, params) {
|
|
107
183
|
try {
|
|
108
184
|
switch (command) {
|
|
@@ -170,20 +246,36 @@ async function handleCommand(command, params) {
|
|
|
170
246
|
case "search_play": {
|
|
171
247
|
const query = params.query;
|
|
172
248
|
if (!query) return { success: false, error: "Missing search query" };
|
|
173
|
-
const
|
|
174
|
-
if (
|
|
175
|
-
await runAppleScript(`tell application "Spotify" to play track "${
|
|
249
|
+
const result = await spotifySearch(query);
|
|
250
|
+
if (result) {
|
|
251
|
+
await runAppleScript(`tell application "Spotify" to play track "${result.uri}"`);
|
|
176
252
|
await new Promise((r) => setTimeout(r, 1500));
|
|
177
253
|
try {
|
|
178
254
|
const track = await runAppleScript('tell application "Spotify" to name of current track');
|
|
179
255
|
const artist = await runAppleScript('tell application "Spotify" to artist of current track');
|
|
180
|
-
|
|
256
|
+
const state = await runAppleScript('tell application "Spotify" to player state as string');
|
|
257
|
+
return {
|
|
258
|
+
success: true,
|
|
259
|
+
data: {
|
|
260
|
+
searched: query,
|
|
261
|
+
resolved: `${result.name} - ${result.artist}`,
|
|
262
|
+
nowPlaying: `${track} - ${artist}`,
|
|
263
|
+
state
|
|
264
|
+
}
|
|
265
|
+
};
|
|
181
266
|
} catch {
|
|
182
|
-
return { success: true, data: { searched: query, note: "Playing track" } };
|
|
267
|
+
return { success: true, data: { searched: query, resolved: `${result.name} - ${result.artist}`, note: "Playing track" } };
|
|
183
268
|
}
|
|
184
269
|
}
|
|
185
270
|
await runShell(`open "spotify:search:${encodeURIComponent(query)}"`);
|
|
186
|
-
return {
|
|
271
|
+
return {
|
|
272
|
+
success: true,
|
|
273
|
+
data: {
|
|
274
|
+
searched: query,
|
|
275
|
+
note: "Search engine unavailable. Opened Spotify search \u2014 use computer-use (screenshot + click) to select the correct song.",
|
|
276
|
+
requiresComputerUse: true
|
|
277
|
+
}
|
|
278
|
+
};
|
|
187
279
|
}
|
|
188
280
|
case "volume": {
|
|
189
281
|
const level = params.level;
|