@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.
Files changed (2) hide show
  1. package/dist/index.js +134 -42
  2. 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 spotifyToken = null;
59
- var spotifyTokenExpiry = 0;
60
- async function getSpotifyToken() {
61
- if (spotifyToken && Date.now() < spotifyTokenExpiry) return spotifyToken;
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("https://open.spotify.com/embed/track/4u7EnebtmKWzUH433cf5Qv", {
64
- headers: { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36" }
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 match = html.match(/"accessToken":"([^"]+)"/);
68
- if (!match) return null;
69
- spotifyToken = match[1];
70
- spotifyTokenExpiry = Date.now() + 55 * 60 * 1e3;
71
- return spotifyToken;
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 null;
116
+ return [];
74
117
  }
75
118
  }
76
- async function spotifySearch(query) {
77
- const token = await getSpotifyToken();
78
- if (!token) return null;
119
+ async function searchStartpageForSpotifyTracks(query) {
79
120
  try {
80
- const res = await fetch(
81
- `https://api.spotify.com/v1/search?q=${encodeURIComponent(query)}&type=track&limit=1`,
82
- { headers: { Authorization: `Bearer ${token}` } }
83
- );
84
- if (!res.ok) {
85
- if (res.status === 401) {
86
- spotifyToken = null;
87
- spotifyTokenExpiry = 0;
88
- const newToken = await getSpotifyToken();
89
- if (!newToken) return null;
90
- const retry = await fetch(
91
- `https://api.spotify.com/v1/search?q=${encodeURIComponent(query)}&type=track&limit=1`,
92
- { headers: { Authorization: `Bearer ${newToken}` } }
93
- );
94
- if (!retry.ok) return null;
95
- const retryData = await retry.json();
96
- return retryData.tracks?.items?.[0]?.uri ?? null;
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
- const data = await res.json();
101
- return data.tracks?.items?.[0]?.uri ?? null;
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 trackUri = await spotifySearch(query);
174
- if (trackUri) {
175
- await runAppleScript(`tell application "Spotify" to play track "${trackUri}"`);
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
- return { success: true, data: { searched: query, nowPlaying: `${track} - ${artist}`, state: "playing" } };
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 { success: true, data: { searched: query, note: "Opened Spotify search (API unavailable). Select a song to play." } };
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pulso/companion",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "description": "Pulso Companion — gives your AI agent real control over your computer",
6
6
  "bin": {