@leg3ndy/otto-bridge 0.8.1 → 0.8.3
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/executors/native_macos.js +859 -56
- package/dist/http.js +6 -0
- package/dist/local_automations.js +559 -0
- package/dist/macos_whatsapp_helper.js +113 -43
- package/dist/runtime.js +8 -0
- package/dist/types.js +1 -1
- package/dist/whatsapp_background.js +109 -43
- package/dist/whatsapp_verification.js +66 -0
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@ import { JobCancelledError } from "./shared.js";
|
|
|
7
7
|
import { loadManagedBridgeExtensionState, saveManagedBridgeExtensionState, } from "../extensions.js";
|
|
8
8
|
import { postDeviceJson, uploadDeviceJobArtifact } from "../http.js";
|
|
9
9
|
import { WHATSAPP_WEB_URL, WhatsAppBackgroundBrowser, } from "../whatsapp_background.js";
|
|
10
|
+
import { verifyExpectedWhatsAppMessage } from "../whatsapp_verification.js";
|
|
10
11
|
const KNOWN_APPS = [
|
|
11
12
|
{ canonical: "Safari", patterns: [/\bsafari\b/i] },
|
|
12
13
|
{ canonical: "Google Chrome", patterns: [/\bgoogle chrome\b/i, /\bchrome\b/i] },
|
|
@@ -221,6 +222,136 @@ function descriptionWantsPause(description) {
|
|
|
221
222
|
function descriptionWantsResume(description) {
|
|
222
223
|
return /\b(retoma|retomar|resume|continu[ae]r|despausa|play)\b/.test(normalizeText(description || ""));
|
|
223
224
|
}
|
|
225
|
+
function descriptionWantsSpotifyTrackPageOpen(description) {
|
|
226
|
+
const normalized = normalizeText(description || "");
|
|
227
|
+
return /\bspotify\b/.test(normalized)
|
|
228
|
+
&& /\b(pagina|titulo|nome|link)\b/.test(normalized)
|
|
229
|
+
&& /\b(faixa|musica|track)\b/.test(normalized);
|
|
230
|
+
}
|
|
231
|
+
function descriptionWantsSpotifyTrackPagePlay(description) {
|
|
232
|
+
const normalized = normalizeText(description || "");
|
|
233
|
+
return /\bspotify\b/.test(normalized)
|
|
234
|
+
&& /\b(play|reproduzir|tocar|verde|principal)\b/.test(normalized)
|
|
235
|
+
&& /\b(pagina|faixa|musica|track)\b/.test(normalized);
|
|
236
|
+
}
|
|
237
|
+
function isSpotifySafariDomOnlyStep(description) {
|
|
238
|
+
return descriptionWantsSpotifyTrackPageOpen(description) || descriptionWantsSpotifyTrackPagePlay(description);
|
|
239
|
+
}
|
|
240
|
+
function playerStateLooksActive(playerState) {
|
|
241
|
+
const normalized = normalizeText(playerState || "");
|
|
242
|
+
return normalized.includes("pause") || normalized.includes("pausar");
|
|
243
|
+
}
|
|
244
|
+
function browserStateMatchesSpotifyTrackDescription(state, description) {
|
|
245
|
+
if (!state) {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
const normalizedUrl = normalizeComparableUrl(state.url || "");
|
|
249
|
+
if (!normalizedUrl.includes("open.spotify.com/track/")) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
const haystack = [
|
|
253
|
+
state.playerTitle || "",
|
|
254
|
+
state.title || "",
|
|
255
|
+
String(state.text || "").slice(0, 4000),
|
|
256
|
+
].join(" ");
|
|
257
|
+
const normalizedHaystack = normalizeText(haystack);
|
|
258
|
+
if (!normalizedHaystack) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
const quotedPhrases = extractQuotedPhrases(description);
|
|
262
|
+
if (quotedPhrases.some((phrase) => normalizedHaystack.includes(phrase))) {
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
const mediaQueryTokens = extractMediaQueryTokens(description);
|
|
266
|
+
if (!mediaQueryTokens.length) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
const matchCount = countMatchingTokens(haystack, mediaQueryTokens);
|
|
270
|
+
return matchCount >= Math.max(1, Math.ceil(mediaQueryTokens.length * 0.5));
|
|
271
|
+
}
|
|
272
|
+
function spotifyStateContentMatchesDescription(state, description) {
|
|
273
|
+
if (!state) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
const normalizedUrl = normalizeComparableUrl(state.url || "");
|
|
277
|
+
if (!normalizedUrl.includes("open.spotify.com")) {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
const haystack = [
|
|
281
|
+
state.playerTitle || "",
|
|
282
|
+
state.title || "",
|
|
283
|
+
String(state.text || "").slice(0, 4000),
|
|
284
|
+
].join(" ");
|
|
285
|
+
const normalizedHaystack = normalizeText(haystack);
|
|
286
|
+
if (!normalizedHaystack) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
const quotedPhrases = extractQuotedPhrases(description);
|
|
290
|
+
if (quotedPhrases.some((phrase) => normalizedHaystack.includes(phrase))) {
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
const mediaQueryTokens = extractMediaQueryTokens(description);
|
|
294
|
+
if (!mediaQueryTokens.length) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
const matchCount = countMatchingTokens(haystack, mediaQueryTokens);
|
|
298
|
+
return matchCount >= Math.max(1, Math.ceil(mediaQueryTokens.length * 0.5));
|
|
299
|
+
}
|
|
300
|
+
function spotifyPlayerAlreadyPlayingMatchesDescription(state, description) {
|
|
301
|
+
if (!state) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
const normalizedUrl = normalizeComparableUrl(state.url || "");
|
|
305
|
+
if (!normalizedUrl.includes("open.spotify.com")) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
if (!playerStateLooksActive(state.playerState || "")) {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
const haystack = [
|
|
312
|
+
state.playerTitle || "",
|
|
313
|
+
state.title || "",
|
|
314
|
+
String(state.text || "").slice(0, 4000),
|
|
315
|
+
].join(" ");
|
|
316
|
+
const normalizedHaystack = normalizeText(haystack);
|
|
317
|
+
if (!normalizedHaystack) {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
const quotedPhrases = extractQuotedPhrases(description);
|
|
321
|
+
if (quotedPhrases.some((phrase) => normalizedHaystack.includes(phrase))) {
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
const mediaQueryTokens = extractMediaQueryTokens(description);
|
|
325
|
+
if (!mediaQueryTokens.length) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
const matchCount = countMatchingTokens(haystack, mediaQueryTokens);
|
|
329
|
+
return matchCount >= Math.max(1, Math.ceil(mediaQueryTokens.length * 0.5));
|
|
330
|
+
}
|
|
331
|
+
function spotifyTrackAlreadyPlayingMatchesDescription(state, description) {
|
|
332
|
+
return browserStateMatchesSpotifyTrackDescription(state, description)
|
|
333
|
+
&& spotifyPlayerAlreadyPlayingMatchesDescription(state, description);
|
|
334
|
+
}
|
|
335
|
+
function spotifyTrackPageLooksPlaying(state, description) {
|
|
336
|
+
return browserStateMatchesSpotifyTrackDescription(state, description)
|
|
337
|
+
&& playerStateLooksActive(state?.playerState || "");
|
|
338
|
+
}
|
|
339
|
+
function spotifyDescriptionsLikelyMatch(left, right) {
|
|
340
|
+
const leftPhrases = new Set(extractQuotedPhrases(left));
|
|
341
|
+
const rightPhrases = new Set(extractQuotedPhrases(right));
|
|
342
|
+
for (const phrase of leftPhrases) {
|
|
343
|
+
if (rightPhrases.has(phrase)) {
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const leftTokens = extractMediaQueryTokens(left);
|
|
348
|
+
const rightTokens = new Set(extractMediaQueryTokens(right));
|
|
349
|
+
if (!leftTokens.length || !rightTokens.size) {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
const overlap = leftTokens.filter((token) => rightTokens.has(token)).length;
|
|
353
|
+
return overlap >= Math.max(1, Math.ceil(Math.min(leftTokens.length, rightTokens.size) * 0.5));
|
|
354
|
+
}
|
|
224
355
|
function extractNativeMediaTransportCommand(description) {
|
|
225
356
|
const normalizedDescription = normalizeText(description || "");
|
|
226
357
|
if (!normalizedDescription
|
|
@@ -473,6 +604,10 @@ function urlHostname(url) {
|
|
|
473
604
|
return null;
|
|
474
605
|
}
|
|
475
606
|
}
|
|
607
|
+
function isSpotifySearchUrl(url) {
|
|
608
|
+
const normalized = normalizeComparableUrl(normalizeUrl(url));
|
|
609
|
+
return normalized.includes("open.spotify.com/search/");
|
|
610
|
+
}
|
|
476
611
|
function uniqueStrings(values) {
|
|
477
612
|
const seen = new Set();
|
|
478
613
|
const result = [];
|
|
@@ -971,6 +1106,9 @@ export class NativeMacOSJobExecutor {
|
|
|
971
1106
|
lastActiveApp = null;
|
|
972
1107
|
lastVisualTargetDescription = null;
|
|
973
1108
|
lastVisualTargetApp = null;
|
|
1109
|
+
lastSatisfiedSpotifyDescription = null;
|
|
1110
|
+
lastSatisfiedSpotifyConfirmedPlaying = false;
|
|
1111
|
+
lastSatisfiedSpotifyAt = 0;
|
|
974
1112
|
whatsappBackgroundBrowser = null;
|
|
975
1113
|
whatsappRuntimeMonitor = null;
|
|
976
1114
|
constructor(bridgeConfig) {
|
|
@@ -987,6 +1125,9 @@ export class NativeMacOSJobExecutor {
|
|
|
987
1125
|
if (actions.length === 0) {
|
|
988
1126
|
throw new Error("Otto Bridge native-macos could not derive a supported local action from this request");
|
|
989
1127
|
}
|
|
1128
|
+
this.lastSatisfiedSpotifyDescription = null;
|
|
1129
|
+
this.lastSatisfiedSpotifyConfirmedPlaying = false;
|
|
1130
|
+
this.lastSatisfiedSpotifyAt = 0;
|
|
990
1131
|
await reporter.accepted();
|
|
991
1132
|
const confirmation = extractConfirmationOptions(job, actions);
|
|
992
1133
|
if (confirmation.required) {
|
|
@@ -1025,7 +1166,7 @@ export class NativeMacOSJobExecutor {
|
|
|
1025
1166
|
}
|
|
1026
1167
|
if (action.type === "press_shortcut") {
|
|
1027
1168
|
await reporter.progress(progressPercent, `Enviando atalho ${action.shortcut}`);
|
|
1028
|
-
await this.pressShortcut(action.shortcut);
|
|
1169
|
+
const shortcutResult = await this.pressShortcut(action.shortcut);
|
|
1029
1170
|
if (action.shortcut.startsWith("media_")) {
|
|
1030
1171
|
const mediaSummaryMap = {
|
|
1031
1172
|
media_next: "Acionei o comando de próxima mídia no macOS.",
|
|
@@ -1035,7 +1176,14 @@ export class NativeMacOSJobExecutor {
|
|
|
1035
1176
|
media_play: "Acionei o comando de reproduzir mídia no macOS.",
|
|
1036
1177
|
media_play_pause: "Acionei o comando de play/pause de mídia no macOS.",
|
|
1037
1178
|
};
|
|
1038
|
-
|
|
1179
|
+
const mediaSkippedSummaryMap = {
|
|
1180
|
+
media_pause: "Nenhuma mídia estava tocando no macOS, então o pause global foi pulado.",
|
|
1181
|
+
media_resume: "Já havia mídia tocando no macOS, então o comando de retomar foi pulado.",
|
|
1182
|
+
media_play: "Já havia mídia tocando no macOS, então o comando de reproduzir foi pulado.",
|
|
1183
|
+
};
|
|
1184
|
+
completionNotes.push(shortcutResult.performed
|
|
1185
|
+
? (mediaSummaryMap[action.shortcut] || `Acionei ${action.shortcut} no macOS.`)
|
|
1186
|
+
: (mediaSkippedSummaryMap[action.shortcut] || shortcutResult.reason || `O atalho ${action.shortcut} foi pulado no macOS.`));
|
|
1039
1187
|
}
|
|
1040
1188
|
continue;
|
|
1041
1189
|
}
|
|
@@ -1197,10 +1345,6 @@ export class NativeMacOSJobExecutor {
|
|
|
1197
1345
|
await reporter.progress(progressPercent, `Enviando a mensagem para ${action.contact} no WhatsApp`);
|
|
1198
1346
|
await this.sendWhatsAppMessage(action.text);
|
|
1199
1347
|
await delay(900);
|
|
1200
|
-
const verification = await this.verifyWhatsAppLastMessageAgainstBaseline(action.text, beforeSend.messages);
|
|
1201
|
-
if (!verification.ok) {
|
|
1202
|
-
throw new Error(verification.reason || `Nao consegui confirmar o envio da mensagem para ${action.contact} no WhatsApp.`);
|
|
1203
|
-
}
|
|
1204
1348
|
const afterSend = await this.readWhatsAppVisibleConversation(action.contact, Math.max(12, beforeSend.messages.length + 4)).catch(() => null);
|
|
1205
1349
|
resultPayload.whatsapp = {
|
|
1206
1350
|
action: "send_message",
|
|
@@ -1209,6 +1353,12 @@ export class NativeMacOSJobExecutor {
|
|
|
1209
1353
|
messages: afterSend?.messages || [],
|
|
1210
1354
|
summary: afterSend?.summary || "",
|
|
1211
1355
|
};
|
|
1356
|
+
const verification = await this.verifyWhatsAppLastMessageAgainstBaseline(action.text, beforeSend.messages);
|
|
1357
|
+
if (!verification.ok) {
|
|
1358
|
+
resultPayload.summary = verification.reason || `Nao consegui confirmar o envio da mensagem para ${action.contact} no WhatsApp.`;
|
|
1359
|
+
await reporter.failed(verification.reason || `Nao consegui confirmar o envio da mensagem para ${action.contact} no WhatsApp.`, resultPayload);
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1212
1362
|
completionNotes.push(`Enviei no WhatsApp para ${action.contact}: ${clipText(action.text, 180)}`);
|
|
1213
1363
|
continue;
|
|
1214
1364
|
}
|
|
@@ -1239,7 +1389,9 @@ export class NativeMacOSJobExecutor {
|
|
|
1239
1389
|
await reporter.progress(progressPercent, `Trazendo ${action.app} para frente antes do clique`);
|
|
1240
1390
|
await this.focusApp(action.app);
|
|
1241
1391
|
}
|
|
1242
|
-
const targetDescriptions =
|
|
1392
|
+
const targetDescriptions = isSpotifySafariDomOnlyStep(action.description)
|
|
1393
|
+
? [action.description]
|
|
1394
|
+
: uniqueStrings([action.description, ...(action.retry_descriptions || [])]);
|
|
1243
1395
|
let clickSucceeded = false;
|
|
1244
1396
|
let lastFailureReason = "";
|
|
1245
1397
|
for (let attempt = 0; attempt < targetDescriptions.length; attempt += 1) {
|
|
@@ -1247,6 +1399,33 @@ export class NativeMacOSJobExecutor {
|
|
|
1247
1399
|
const initialBrowserState = browserApp
|
|
1248
1400
|
? await this.captureBrowserPageState(browserApp).catch(() => null)
|
|
1249
1401
|
: null;
|
|
1402
|
+
const isSpotifySafariStep = browserApp === "Safari" && isSpotifySafariDomOnlyStep(targetDescription);
|
|
1403
|
+
const verificationPrompt = isSpotifySafariStep ? undefined : action.verification_prompt;
|
|
1404
|
+
if (isSpotifySafariStep) {
|
|
1405
|
+
await reporter.progress(progressPercent, `Tentando concluir ${targetDescription} pelo DOM do Spotify no Safari`);
|
|
1406
|
+
const spotifyDomResult = await this.executeSpotifySafariDomStep(targetDescription, initialBrowserState);
|
|
1407
|
+
if (spotifyDomResult.ok) {
|
|
1408
|
+
this.rememberSatisfiedSpotifyStep(targetDescription, !!spotifyDomResult.confirmedPlaying);
|
|
1409
|
+
this.lastVisualTargetDescription = targetDescription;
|
|
1410
|
+
this.lastVisualTargetApp = browserApp || action.app || this.lastActiveApp;
|
|
1411
|
+
resultPayload.last_click = {
|
|
1412
|
+
strategy: spotifyDomResult.skipped
|
|
1413
|
+
? "spotify_browser_state_skip"
|
|
1414
|
+
: (spotifyDomResult.click?.strategy || "spotify_safari_dom"),
|
|
1415
|
+
matched_text: spotifyDomResult.click?.matchedText || targetDescription,
|
|
1416
|
+
matched_href: spotifyDomResult.click?.matchedHref || spotifyDomResult.afterState?.url || null,
|
|
1417
|
+
score: spotifyDomResult.click?.score || null,
|
|
1418
|
+
total_candidates: spotifyDomResult.click?.totalCandidates || null,
|
|
1419
|
+
};
|
|
1420
|
+
completionNotes.push(spotifyDomResult.skipped
|
|
1421
|
+
? `O Spotify Web ja estava no estado certo para ${targetDescription}, então pulei tentativas extras.`
|
|
1422
|
+
: `Concluí ${targetDescription} diretamente pelo DOM do Spotify Web no Safari.`);
|
|
1423
|
+
clickSucceeded = true;
|
|
1424
|
+
break;
|
|
1425
|
+
}
|
|
1426
|
+
lastFailureReason = spotifyDomResult.reason || `Nao consegui concluir ${targetDescription} pelo DOM do Spotify Web no Safari.`;
|
|
1427
|
+
continue;
|
|
1428
|
+
}
|
|
1250
1429
|
const nativeMediaTransport = extractNativeMediaTransportCommand(targetDescription);
|
|
1251
1430
|
if (nativeMediaTransport) {
|
|
1252
1431
|
await reporter.progress(progressPercent, `Tentando controle de mídia nativo do macOS para ${targetDescription}`);
|
|
@@ -1259,8 +1438,8 @@ export class NativeMacOSJobExecutor {
|
|
|
1259
1438
|
validated = browserValidation.ok;
|
|
1260
1439
|
validationReason = browserValidation.reason;
|
|
1261
1440
|
}
|
|
1262
|
-
if (!validated &&
|
|
1263
|
-
const verification = await this.validateVisualClickWithVision(job.job_id, targetDescription,
|
|
1441
|
+
if (!validated && verificationPrompt) {
|
|
1442
|
+
const verification = await this.validateVisualClickWithVision(job.job_id, targetDescription, verificationPrompt, progressPercent, reporter, artifacts, "native_media_transport_result");
|
|
1264
1443
|
if (verification.unavailable) {
|
|
1265
1444
|
lastFailureReason = verification.reason;
|
|
1266
1445
|
break;
|
|
@@ -1298,8 +1477,8 @@ export class NativeMacOSJobExecutor {
|
|
|
1298
1477
|
validated = browserValidation.ok;
|
|
1299
1478
|
validationReason = browserValidation.reason;
|
|
1300
1479
|
}
|
|
1301
|
-
if (!validated &&
|
|
1302
|
-
const verification = await this.validateVisualClickWithVision(job.job_id, targetDescription,
|
|
1480
|
+
if (!validated && verificationPrompt) {
|
|
1481
|
+
const verification = await this.validateVisualClickWithVision(job.job_id, targetDescription, verificationPrompt, progressPercent, reporter, artifacts, "dom_click_result");
|
|
1303
1482
|
if (verification.unavailable) {
|
|
1304
1483
|
lastFailureReason = verification.reason;
|
|
1305
1484
|
break;
|
|
@@ -1344,8 +1523,8 @@ export class NativeMacOSJobExecutor {
|
|
|
1344
1523
|
validated = browserValidation.ok;
|
|
1345
1524
|
validationReason = browserValidation.reason;
|
|
1346
1525
|
}
|
|
1347
|
-
if (!validated &&
|
|
1348
|
-
const verification = await this.validateVisualClickWithVision(job.job_id, targetDescription,
|
|
1526
|
+
if (!validated && verificationPrompt) {
|
|
1527
|
+
const verification = await this.validateVisualClickWithVision(job.job_id, targetDescription, verificationPrompt, progressPercent, reporter, artifacts, "local_ocr_click_result");
|
|
1349
1528
|
if (verification.unavailable) {
|
|
1350
1529
|
lastFailureReason = verification.reason;
|
|
1351
1530
|
break;
|
|
@@ -1425,8 +1604,8 @@ export class NativeMacOSJobExecutor {
|
|
|
1425
1604
|
y: scaledY,
|
|
1426
1605
|
strategy: "visual_locator",
|
|
1427
1606
|
};
|
|
1428
|
-
if (
|
|
1429
|
-
const verification = await this.validateVisualClickWithVision(job.job_id, targetDescription,
|
|
1607
|
+
if (verificationPrompt) {
|
|
1608
|
+
const verification = await this.validateVisualClickWithVision(job.job_id, targetDescription, verificationPrompt, progressPercent, reporter, artifacts, "visual_click_result");
|
|
1430
1609
|
if (verification.unavailable) {
|
|
1431
1610
|
lastFailureReason = verification.reason;
|
|
1432
1611
|
break;
|
|
@@ -1531,6 +1710,9 @@ export class NativeMacOSJobExecutor {
|
|
|
1531
1710
|
await this.runCommand("open", ["-a", app, url]);
|
|
1532
1711
|
}
|
|
1533
1712
|
await this.focusApp(app);
|
|
1713
|
+
if (isSpotifySearchUrl(url)) {
|
|
1714
|
+
await this.ensureSafariSpotifySearchUrl(url);
|
|
1715
|
+
}
|
|
1534
1716
|
this.lastActiveApp = app;
|
|
1535
1717
|
return;
|
|
1536
1718
|
}
|
|
@@ -1542,6 +1724,48 @@ export class NativeMacOSJobExecutor {
|
|
|
1542
1724
|
}
|
|
1543
1725
|
await this.runCommand("open", [url]);
|
|
1544
1726
|
}
|
|
1727
|
+
async ensureSafariSpotifySearchUrl(url) {
|
|
1728
|
+
const targetUrl = normalizeUrl(url);
|
|
1729
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1730
|
+
await delay(700);
|
|
1731
|
+
const currentUrl = await this.readSafariCurrentUrl().catch(() => "");
|
|
1732
|
+
const normalizedCurrentUrl = normalizeComparableUrl(currentUrl);
|
|
1733
|
+
if (!normalizedCurrentUrl || !normalizedCurrentUrl.includes("open.spotify.com")) {
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
if (normalizedCurrentUrl.includes("/search/")) {
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
if (!/open\.spotify\.com\/?$/.test(normalizedCurrentUrl)) {
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
await this.setSafariFrontTabUrl(targetUrl).catch(() => undefined);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
async readSafariCurrentUrl() {
|
|
1746
|
+
const script = `
|
|
1747
|
+
tell application "Safari"
|
|
1748
|
+
if (count of windows) = 0 then return ""
|
|
1749
|
+
try
|
|
1750
|
+
return URL of current tab of front window
|
|
1751
|
+
on error
|
|
1752
|
+
return ""
|
|
1753
|
+
end try
|
|
1754
|
+
end tell
|
|
1755
|
+
`;
|
|
1756
|
+
const { stdout } = await this.runCommandCapture("osascript", ["-e", script]);
|
|
1757
|
+
return String(stdout || "").trim();
|
|
1758
|
+
}
|
|
1759
|
+
async setSafariFrontTabUrl(url) {
|
|
1760
|
+
const script = `
|
|
1761
|
+
set targetUrl to "${escapeAppleScript(url)}"
|
|
1762
|
+
tell application "Safari"
|
|
1763
|
+
if (count of windows) = 0 then return
|
|
1764
|
+
set URL of current tab of front window to targetUrl
|
|
1765
|
+
end tell
|
|
1766
|
+
`;
|
|
1767
|
+
await this.runCommand("osascript", ["-e", script]);
|
|
1768
|
+
}
|
|
1545
1769
|
async tryReuseSafariTab(url) {
|
|
1546
1770
|
const targetUrl = normalizeUrl(url);
|
|
1547
1771
|
const targetHost = urlHostname(targetUrl);
|
|
@@ -1651,6 +1875,161 @@ end tell
|
|
|
1651
1875
|
playerState: page.playerState || "",
|
|
1652
1876
|
};
|
|
1653
1877
|
}
|
|
1878
|
+
rememberSatisfiedSpotifyStep(description, confirmedPlaying) {
|
|
1879
|
+
if (!isSpotifySafariDomOnlyStep(description)) {
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
this.lastSatisfiedSpotifyDescription = description;
|
|
1883
|
+
this.lastSatisfiedSpotifyConfirmedPlaying = confirmedPlaying;
|
|
1884
|
+
this.lastSatisfiedSpotifyAt = Date.now();
|
|
1885
|
+
}
|
|
1886
|
+
hasRecentSatisfiedSpotifyPlayback(description, maxAgeMs = 10_000) {
|
|
1887
|
+
if (!this.lastSatisfiedSpotifyConfirmedPlaying || !this.lastSatisfiedSpotifyDescription || !this.lastSatisfiedSpotifyAt) {
|
|
1888
|
+
return false;
|
|
1889
|
+
}
|
|
1890
|
+
if ((Date.now() - this.lastSatisfiedSpotifyAt) > maxAgeMs) {
|
|
1891
|
+
return false;
|
|
1892
|
+
}
|
|
1893
|
+
return spotifyDescriptionsLikelyMatch(this.lastSatisfiedSpotifyDescription, description);
|
|
1894
|
+
}
|
|
1895
|
+
hasRecentSatisfiedSpotifyStep(description, maxAgeMs = 10_000) {
|
|
1896
|
+
if (!this.lastSatisfiedSpotifyDescription || !this.lastSatisfiedSpotifyAt) {
|
|
1897
|
+
return false;
|
|
1898
|
+
}
|
|
1899
|
+
if ((Date.now() - this.lastSatisfiedSpotifyAt) > maxAgeMs) {
|
|
1900
|
+
return false;
|
|
1901
|
+
}
|
|
1902
|
+
return spotifyDescriptionsLikelyMatch(this.lastSatisfiedSpotifyDescription, description);
|
|
1903
|
+
}
|
|
1904
|
+
async executeSpotifySafariDomStep(targetDescription, initialBrowserState) {
|
|
1905
|
+
if (descriptionWantsSpotifyTrackPagePlay(targetDescription)
|
|
1906
|
+
&& this.hasRecentSatisfiedSpotifyStep(targetDescription)
|
|
1907
|
+
&& browserStateMatchesSpotifyTrackDescription(initialBrowserState, targetDescription)) {
|
|
1908
|
+
return {
|
|
1909
|
+
ok: true,
|
|
1910
|
+
skipped: true,
|
|
1911
|
+
afterState: initialBrowserState,
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
if (descriptionWantsSpotifyTrackPagePlay(targetDescription) && this.hasRecentSatisfiedSpotifyPlayback(targetDescription)) {
|
|
1915
|
+
return {
|
|
1916
|
+
ok: true,
|
|
1917
|
+
skipped: true,
|
|
1918
|
+
confirmedPlaying: true,
|
|
1919
|
+
afterState: initialBrowserState,
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
if (spotifyPlayerAlreadyPlayingMatchesDescription(initialBrowserState, targetDescription)) {
|
|
1923
|
+
return {
|
|
1924
|
+
ok: true,
|
|
1925
|
+
skipped: true,
|
|
1926
|
+
confirmedPlaying: true,
|
|
1927
|
+
afterState: initialBrowserState,
|
|
1928
|
+
};
|
|
1929
|
+
}
|
|
1930
|
+
if (descriptionWantsSpotifyTrackPagePlay(targetDescription)) {
|
|
1931
|
+
const shouldWaitLongerForAutoplay = this.hasRecentSatisfiedSpotifyStep(targetDescription);
|
|
1932
|
+
let settledState = initialBrowserState;
|
|
1933
|
+
for (let attempt = 0; attempt < (shouldWaitLongerForAutoplay ? 6 : 3); attempt += 1) {
|
|
1934
|
+
if (attempt > 0) {
|
|
1935
|
+
await delay(shouldWaitLongerForAutoplay ? 550 : 450);
|
|
1936
|
+
}
|
|
1937
|
+
settledState = await this.captureBrowserPageState("Safari").catch(() => settledState);
|
|
1938
|
+
const playbackState = await this.readGlobalMediaPlaybackState();
|
|
1939
|
+
if ((playbackState === "playing" && spotifyStateContentMatchesDescription(settledState, targetDescription))
|
|
1940
|
+
|| spotifyTrackPageLooksPlaying(settledState, targetDescription)
|
|
1941
|
+
|| spotifyTrackAlreadyPlayingMatchesDescription(settledState, targetDescription)) {
|
|
1942
|
+
return {
|
|
1943
|
+
ok: true,
|
|
1944
|
+
skipped: true,
|
|
1945
|
+
confirmedPlaying: true,
|
|
1946
|
+
afterState: settledState,
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
const alreadyOnRequestedTrack = browserStateMatchesSpotifyTrackDescription(initialBrowserState, targetDescription);
|
|
1951
|
+
if (alreadyOnRequestedTrack) {
|
|
1952
|
+
const playbackState = await this.readGlobalMediaPlaybackState();
|
|
1953
|
+
if (playbackState === "playing"
|
|
1954
|
+
|| spotifyTrackPageLooksPlaying(initialBrowserState, targetDescription)
|
|
1955
|
+
|| spotifyTrackAlreadyPlayingMatchesDescription(initialBrowserState, targetDescription)) {
|
|
1956
|
+
return {
|
|
1957
|
+
ok: true,
|
|
1958
|
+
skipped: true,
|
|
1959
|
+
confirmedPlaying: true,
|
|
1960
|
+
afterState: initialBrowserState,
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
const domClick = await this.trySafariDomClick(targetDescription);
|
|
1966
|
+
let currentState = await this.captureBrowserPageState("Safari").catch(() => initialBrowserState);
|
|
1967
|
+
if (!domClick?.clicked) {
|
|
1968
|
+
return {
|
|
1969
|
+
ok: false,
|
|
1970
|
+
reason: domClick?.reason || `Nao consegui concluir ${targetDescription} pelo DOM do Spotify Web no Safari.`,
|
|
1971
|
+
afterState: currentState,
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
1975
|
+
if (attempt > 0) {
|
|
1976
|
+
await delay(500);
|
|
1977
|
+
}
|
|
1978
|
+
currentState = await this.captureBrowserPageState("Safari").catch(() => currentState);
|
|
1979
|
+
if (descriptionWantsSpotifyTrackPageOpen(targetDescription)) {
|
|
1980
|
+
if (browserStateMatchesSpotifyTrackDescription(currentState, targetDescription)) {
|
|
1981
|
+
return {
|
|
1982
|
+
ok: true,
|
|
1983
|
+
click: domClick,
|
|
1984
|
+
afterState: currentState,
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
const playbackState = await this.readGlobalMediaPlaybackState();
|
|
1988
|
+
if (playbackState === "playing" && spotifyStateContentMatchesDescription(currentState, targetDescription)) {
|
|
1989
|
+
return {
|
|
1990
|
+
ok: true,
|
|
1991
|
+
click: domClick,
|
|
1992
|
+
confirmedPlaying: true,
|
|
1993
|
+
afterState: currentState,
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
if (descriptionWantsSpotifyTrackPagePlay(targetDescription)) {
|
|
1998
|
+
if (spotifyPlayerAlreadyPlayingMatchesDescription(currentState, targetDescription)) {
|
|
1999
|
+
return {
|
|
2000
|
+
ok: true,
|
|
2001
|
+
click: domClick,
|
|
2002
|
+
confirmedPlaying: true,
|
|
2003
|
+
afterState: currentState,
|
|
2004
|
+
};
|
|
2005
|
+
}
|
|
2006
|
+
const playbackState = await this.readGlobalMediaPlaybackState();
|
|
2007
|
+
if (playbackState === "playing" && spotifyStateContentMatchesDescription(currentState, targetDescription)) {
|
|
2008
|
+
return {
|
|
2009
|
+
ok: true,
|
|
2010
|
+
click: domClick,
|
|
2011
|
+
confirmedPlaying: true,
|
|
2012
|
+
afterState: currentState,
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
if (descriptionWantsSpotifyTrackPageOpen(targetDescription)) {
|
|
2018
|
+
const matchedHref = normalizeComparableUrl(domClick.matchedHref || "");
|
|
2019
|
+
if (matchedHref.includes("/track/")) {
|
|
2020
|
+
return {
|
|
2021
|
+
ok: true,
|
|
2022
|
+
click: domClick,
|
|
2023
|
+
afterState: currentState,
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
return {
|
|
2028
|
+
ok: false,
|
|
2029
|
+
reason: `O clique em ${targetDescription} nao mudou a pagina do navegador de forma verificavel.`,
|
|
2030
|
+
afterState: currentState,
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
1654
2033
|
resolveExpectedBrowserHref(rawHref, baseUrl) {
|
|
1655
2034
|
const href = String(rawHref || "").trim();
|
|
1656
2035
|
if (!href) {
|
|
@@ -1691,12 +2070,14 @@ end tell
|
|
|
1691
2070
|
const afterPlayerTitle = normalizeText(after.playerTitle || "");
|
|
1692
2071
|
const beforePlayerState = normalizeText(before?.playerState || "");
|
|
1693
2072
|
const afterPlayerState = normalizeText(after.playerState || "");
|
|
1694
|
-
const playerLooksActive =
|
|
2073
|
+
const playerLooksActive = playerStateLooksActive(after.playerState || "");
|
|
1695
2074
|
const playerLooksPaused = !playerLooksActive && /play|tocar|reproduzir|continuar|retomar|resume/.test(afterPlayerState);
|
|
1696
2075
|
const wantsNext = descriptionWantsNext(targetDescription);
|
|
1697
2076
|
const wantsPrevious = descriptionWantsPrevious(targetDescription);
|
|
1698
2077
|
const wantsPause = descriptionWantsPause(targetDescription);
|
|
1699
2078
|
const wantsResume = descriptionWantsResume(targetDescription);
|
|
2079
|
+
const wantsSpotifyTrackPageOpen = descriptionWantsSpotifyTrackPageOpen(targetDescription);
|
|
2080
|
+
const wantsSpotifyTrackPagePlay = descriptionWantsSpotifyTrackPagePlay(targetDescription);
|
|
1700
2081
|
const mediaQueryTokens = extractMediaQueryTokens(targetDescription);
|
|
1701
2082
|
const mediaMatchCount = countMatchingTokens(after.playerTitle || "", mediaQueryTokens);
|
|
1702
2083
|
if (afterUrl.includes("music.youtube.com")) {
|
|
@@ -1722,6 +2103,51 @@ end tell
|
|
|
1722
2103
|
return true;
|
|
1723
2104
|
}
|
|
1724
2105
|
}
|
|
2106
|
+
if (afterUrl.includes("open.spotify.com")) {
|
|
2107
|
+
if (wantsSpotifyTrackPageOpen && spotifyPlayerAlreadyPlayingMatchesDescription(after, targetDescription)) {
|
|
2108
|
+
return true;
|
|
2109
|
+
}
|
|
2110
|
+
if (wantsSpotifyTrackPagePlay && spotifyTrackAlreadyPlayingMatchesDescription(after, targetDescription)) {
|
|
2111
|
+
return true;
|
|
2112
|
+
}
|
|
2113
|
+
if (wantsSpotifyTrackPageOpen) {
|
|
2114
|
+
const beforeWasTrackPage = beforeUrl.includes("/track/");
|
|
2115
|
+
const afterIsTrackPage = afterUrl.includes("/track/");
|
|
2116
|
+
if (afterIsTrackPage && (!beforeWasTrackPage || beforeUrl !== afterUrl)) {
|
|
2117
|
+
return true;
|
|
2118
|
+
}
|
|
2119
|
+
return spotifyPlayerAlreadyPlayingMatchesDescription(after, targetDescription);
|
|
2120
|
+
}
|
|
2121
|
+
if (wantsPause && beforePlayerState && beforePlayerState !== afterPlayerState && playerLooksPaused) {
|
|
2122
|
+
return true;
|
|
2123
|
+
}
|
|
2124
|
+
if (wantsResume && playerLooksActive && beforePlayerState !== afterPlayerState) {
|
|
2125
|
+
return true;
|
|
2126
|
+
}
|
|
2127
|
+
if ((wantsNext || wantsPrevious) && beforePlayerTitle && afterPlayerTitle && beforePlayerTitle !== afterPlayerTitle) {
|
|
2128
|
+
return true;
|
|
2129
|
+
}
|
|
2130
|
+
if (mediaQueryTokens.length >= 2) {
|
|
2131
|
+
const requiredMatches = Math.max(2, Math.ceil(mediaQueryTokens.length * 0.5));
|
|
2132
|
+
const titleLooksCorrect = mediaMatchCount >= requiredMatches;
|
|
2133
|
+
const playerChanged = ((beforePlayerTitle && afterPlayerTitle && beforePlayerTitle !== afterPlayerTitle)
|
|
2134
|
+
|| (!beforePlayerTitle && !!afterPlayerTitle)
|
|
2135
|
+
|| (beforePlayerState && afterPlayerState && beforePlayerState !== afterPlayerState));
|
|
2136
|
+
if (titleLooksCorrect && (playerChanged || (beforePlayerTitle === afterPlayerTitle && playerLooksActive))) {
|
|
2137
|
+
return true;
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
if (beforePlayerTitle && afterPlayerTitle && beforePlayerTitle !== afterPlayerTitle && playerLooksActive) {
|
|
2141
|
+
return true;
|
|
2142
|
+
}
|
|
2143
|
+
if (!beforePlayerTitle && afterPlayerTitle && playerLooksActive) {
|
|
2144
|
+
return true;
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
const looksLikeMediaAction = wantsNext || wantsPrevious || wantsPause || wantsResume || mediaQueryTokens.length > 0;
|
|
2148
|
+
if (looksLikeMediaAction && (afterUrl.includes("music.youtube.com") || afterUrl.includes("open.spotify.com"))) {
|
|
2149
|
+
return false;
|
|
2150
|
+
}
|
|
1725
2151
|
const beforeTitle = normalizeText(before?.title || "");
|
|
1726
2152
|
const afterTitle = normalizeText(after.title || "");
|
|
1727
2153
|
if (beforeTitle && afterTitle && beforeTitle !== afterTitle) {
|
|
@@ -1742,9 +2168,11 @@ end tell
|
|
|
1742
2168
|
afterState: null,
|
|
1743
2169
|
};
|
|
1744
2170
|
}
|
|
2171
|
+
let lastAfterState = null;
|
|
1745
2172
|
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
1746
2173
|
await delay(attempt === 0 ? 900 : 700);
|
|
1747
2174
|
const afterState = await this.captureBrowserPageState(app).catch(() => null);
|
|
2175
|
+
lastAfterState = afterState;
|
|
1748
2176
|
if (this.didBrowserPageStateChange(before, afterState, targetDescription, matchedHref)) {
|
|
1749
2177
|
return {
|
|
1750
2178
|
ok: true,
|
|
@@ -1756,7 +2184,7 @@ end tell
|
|
|
1756
2184
|
return {
|
|
1757
2185
|
ok: false,
|
|
1758
2186
|
reason: `O clique em ${targetDescription} nao mudou a pagina do navegador de forma verificavel.`,
|
|
1759
|
-
afterState:
|
|
2187
|
+
afterState: lastAfterState,
|
|
1760
2188
|
};
|
|
1761
2189
|
}
|
|
1762
2190
|
async pressShortcut(shortcut) {
|
|
@@ -1778,8 +2206,28 @@ end tell
|
|
|
1778
2206
|
};
|
|
1779
2207
|
const mediaCommand = mediaCommandMap[normalizedShortcut];
|
|
1780
2208
|
if (mediaCommand) {
|
|
2209
|
+
if (normalizedShortcut === "media_pause") {
|
|
2210
|
+
const playbackState = await this.readGlobalMediaPlaybackState();
|
|
2211
|
+
if (playbackState !== "playing") {
|
|
2212
|
+
return {
|
|
2213
|
+
performed: false,
|
|
2214
|
+
reason: playbackState === "unknown"
|
|
2215
|
+
? "O macOS nao confirmou nenhuma mídia tocando com segurança."
|
|
2216
|
+
: "Nenhuma mídia estava tocando no macOS.",
|
|
2217
|
+
};
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
if (normalizedShortcut === "media_resume" || normalizedShortcut === "media_play") {
|
|
2221
|
+
const playbackState = await this.readGlobalMediaPlaybackState();
|
|
2222
|
+
if (playbackState === "playing") {
|
|
2223
|
+
return {
|
|
2224
|
+
performed: false,
|
|
2225
|
+
reason: "Já havia mídia tocando no macOS.",
|
|
2226
|
+
};
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
1781
2229
|
await this.triggerMacOSMediaTransport(mediaCommand);
|
|
1782
|
-
return;
|
|
2230
|
+
return { performed: true };
|
|
1783
2231
|
}
|
|
1784
2232
|
const namedKeyCodes = {
|
|
1785
2233
|
return: 36,
|
|
@@ -1799,12 +2247,67 @@ end tell
|
|
|
1799
2247
|
"-e",
|
|
1800
2248
|
`tell application "System Events" to key code ${namedKeyCodes[key]}${usingClause}`,
|
|
1801
2249
|
]);
|
|
1802
|
-
return;
|
|
2250
|
+
return { performed: true };
|
|
1803
2251
|
}
|
|
1804
2252
|
await this.runCommand("osascript", [
|
|
1805
2253
|
"-e",
|
|
1806
2254
|
`tell application "System Events" to keystroke "${escapeAppleScript(key)}"${usingClause}`,
|
|
1807
2255
|
]);
|
|
2256
|
+
return { performed: true };
|
|
2257
|
+
}
|
|
2258
|
+
async readGlobalMediaPlaybackState() {
|
|
2259
|
+
const swiftScript = `
|
|
2260
|
+
import Foundation
|
|
2261
|
+
import Dispatch
|
|
2262
|
+
import Darwin
|
|
2263
|
+
|
|
2264
|
+
typealias MRMediaRemoteGetNowPlayingApplicationIsPlayingFunction = @convention(c) (DispatchQueue, @escaping (Bool) -> Void) -> Void
|
|
2265
|
+
|
|
2266
|
+
guard let handle = dlopen("/System/Library/PrivateFrameworks/MediaRemote.framework/MediaRemote", RTLD_NOW) else {
|
|
2267
|
+
print("unknown")
|
|
2268
|
+
exit(0)
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
guard let symbol = dlsym(handle, "MRMediaRemoteGetNowPlayingApplicationIsPlaying") else {
|
|
2272
|
+
print("unknown")
|
|
2273
|
+
exit(0)
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
let fn = unsafeBitCast(symbol, to: MRMediaRemoteGetNowPlayingApplicationIsPlayingFunction.self)
|
|
2277
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
2278
|
+
var isPlaying: Bool? = nil
|
|
2279
|
+
|
|
2280
|
+
fn(DispatchQueue.global()) { value in
|
|
2281
|
+
isPlaying = value
|
|
2282
|
+
semaphore.signal()
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
if semaphore.wait(timeout: .now() + .milliseconds(800)) == .timedOut {
|
|
2286
|
+
print("unknown")
|
|
2287
|
+
exit(0)
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
print(isPlaying == true ? "playing" : "not_playing")
|
|
2291
|
+
`;
|
|
2292
|
+
try {
|
|
2293
|
+
const { stdout } = await this.runCommandCapture("swift", [
|
|
2294
|
+
"-module-cache-path",
|
|
2295
|
+
path.join(os.tmpdir(), "otto-bridge-swift-modcache"),
|
|
2296
|
+
"-e",
|
|
2297
|
+
swiftScript,
|
|
2298
|
+
]);
|
|
2299
|
+
const state = normalizeText(stdout || "").trim();
|
|
2300
|
+
if (state.includes("not_playing")) {
|
|
2301
|
+
return "not_playing";
|
|
2302
|
+
}
|
|
2303
|
+
if (state.includes("playing")) {
|
|
2304
|
+
return "playing";
|
|
2305
|
+
}
|
|
2306
|
+
return "unknown";
|
|
2307
|
+
}
|
|
2308
|
+
catch {
|
|
2309
|
+
return "unknown";
|
|
2310
|
+
}
|
|
1808
2311
|
}
|
|
1809
2312
|
async triggerMacOSMediaTransport(command) {
|
|
1810
2313
|
const keyTypeMap = {
|
|
@@ -2664,42 +3167,7 @@ return { messages: messages.slice(-maxMessages) };
|
|
|
2664
3167
|
}
|
|
2665
3168
|
const baseline = Array.isArray(previousMessages) ? previousMessages : [];
|
|
2666
3169
|
const chat = await this.readWhatsAppVisibleConversation("Contato", Math.max(8, baseline.length + 2));
|
|
2667
|
-
|
|
2668
|
-
return {
|
|
2669
|
-
ok: false,
|
|
2670
|
-
reason: "Nao consegui ler as mensagens visiveis apos o envio no WhatsApp.",
|
|
2671
|
-
};
|
|
2672
|
-
}
|
|
2673
|
-
const normalizedExpected = normalizeText(expectedText).slice(0, 120);
|
|
2674
|
-
const normalizeMessage = (item) => `${normalizeText(item.author)}|${normalizeText(item.text)}`;
|
|
2675
|
-
const beforeSignature = baseline.map(normalizeMessage).join("\n");
|
|
2676
|
-
const afterSignature = chat.messages.map(normalizeMessage).join("\n");
|
|
2677
|
-
const changed = beforeSignature !== afterSignature;
|
|
2678
|
-
const beforeMatches = baseline.filter((item) => normalizeText(item.text).includes(normalizedExpected)).length;
|
|
2679
|
-
const afterMatches = chat.messages.filter((item) => normalizeText(item.text).includes(normalizedExpected)).length;
|
|
2680
|
-
const latest = chat.messages[chat.messages.length - 1] || null;
|
|
2681
|
-
const latestAuthor = normalizeText(latest?.author || "");
|
|
2682
|
-
const latestText = normalizeText(latest?.text || "");
|
|
2683
|
-
const latestMatches = latestText.includes(normalizedExpected) && (latestAuthor === "voce" || latestAuthor === "você");
|
|
2684
|
-
if ((changed && latestMatches) || (changed && afterMatches > beforeMatches)) {
|
|
2685
|
-
return { ok: true, reason: "" };
|
|
2686
|
-
}
|
|
2687
|
-
if (!changed) {
|
|
2688
|
-
return {
|
|
2689
|
-
ok: false,
|
|
2690
|
-
reason: "O WhatsApp nao mostrou mudanca visivel na conversa depois da tentativa de envio.",
|
|
2691
|
-
};
|
|
2692
|
-
}
|
|
2693
|
-
if (afterMatches <= beforeMatches) {
|
|
2694
|
-
return {
|
|
2695
|
-
ok: false,
|
|
2696
|
-
reason: "A conversa mudou, mas nao apareceu uma nova mensagem com o texto esperado no WhatsApp.",
|
|
2697
|
-
};
|
|
2698
|
-
}
|
|
2699
|
-
return {
|
|
2700
|
-
ok: false,
|
|
2701
|
-
reason: "Nao consegui confirmar visualmente a nova mensagem enviada no WhatsApp.",
|
|
2702
|
-
};
|
|
3170
|
+
return verifyExpectedWhatsAppMessage(expectedText, baseline, chat.messages);
|
|
2703
3171
|
}
|
|
2704
3172
|
async takeScreenshot(targetPath) {
|
|
2705
3173
|
const artifactsDir = path.join(os.homedir(), ".otto-bridge", "artifacts");
|
|
@@ -2889,6 +3357,14 @@ const wantsNext = /\\b(proxim[ao]?|next|skip|pular|avanca|avancar)\\b/.test(norm
|
|
|
2889
3357
|
const wantsPrevious = /\\b(anterior|previous|volta[ar]?|back|retorna[ar]?)\\b/.test(normalizedDescription);
|
|
2890
3358
|
const wantsPause = /\\b(pausa|pause|pausar)\\b/.test(normalizedDescription);
|
|
2891
3359
|
const wantsResume = /\\b(retoma|retomar|resume|continu[ae]r|despausa|play)\\b/.test(normalizedDescription);
|
|
3360
|
+
const wantsSpotifyTrackPageOpen = location.hostname.includes("open.spotify.com")
|
|
3361
|
+
&& /\\bspotify\\b/.test(normalizedDescription)
|
|
3362
|
+
&& /\\b(pagina|titulo|nome|link)\\b/.test(normalizedDescription)
|
|
3363
|
+
&& /\\b(faixa|musica|track)\\b/.test(normalizedDescription);
|
|
3364
|
+
const wantsSpotifyTrackPagePlay = location.hostname.includes("open.spotify.com")
|
|
3365
|
+
&& /\\bspotify\\b/.test(normalizedDescription)
|
|
3366
|
+
&& /\\b(play|reproduzir|tocar|verde|principal)\\b/.test(normalizedDescription)
|
|
3367
|
+
&& /\\b(pagina|faixa|musica|track)\\b/.test(normalizedDescription);
|
|
2892
3368
|
const stopWords = new Set([
|
|
2893
3369
|
"o", "a", "os", "as", "um", "uma", "uns", "umas", "de", "da", "do", "das", "dos",
|
|
2894
3370
|
"em", "no", "na", "nos", "nas", "para", "por", "com", "que", "visivel", "visiveis",
|
|
@@ -2998,6 +3474,33 @@ function clickElement(element, strategy, matchedText, matchedHref, score, totalC
|
|
|
2998
3474
|
};
|
|
2999
3475
|
}
|
|
3000
3476
|
|
|
3477
|
+
function pickYouTubeMusicPrimaryCard(element) {
|
|
3478
|
+
let current = element instanceof Element ? element : null;
|
|
3479
|
+
let best = null;
|
|
3480
|
+
let bestScore = -Infinity;
|
|
3481
|
+
while (current && current instanceof HTMLElement && current !== document.body) {
|
|
3482
|
+
const rect = current.getBoundingClientRect();
|
|
3483
|
+
const text = normalize(deriveText(current));
|
|
3484
|
+
let score = 0;
|
|
3485
|
+
if (rect.width >= window.innerWidth * 0.32) score += 40;
|
|
3486
|
+
if (rect.width >= window.innerWidth * 0.42) score += 80;
|
|
3487
|
+
if (rect.left <= window.innerWidth * 0.18) score += 90;
|
|
3488
|
+
if ((rect.left + rect.width) <= window.innerWidth * 0.68) score += 120;
|
|
3489
|
+
if (rect.top <= window.innerHeight * 0.7) score += 40;
|
|
3490
|
+
if (text.includes("mais do youtube")) score -= 260;
|
|
3491
|
+
if (text.includes("playlist")) score -= 40;
|
|
3492
|
+
if (text.includes("podcast")) score -= 40;
|
|
3493
|
+
if (text.includes("musica")) score += 28;
|
|
3494
|
+
if (text.includes("video")) score -= 18;
|
|
3495
|
+
if (score > bestScore) {
|
|
3496
|
+
bestScore = score;
|
|
3497
|
+
best = current;
|
|
3498
|
+
}
|
|
3499
|
+
current = current.parentElement;
|
|
3500
|
+
}
|
|
3501
|
+
return best;
|
|
3502
|
+
}
|
|
3503
|
+
|
|
3001
3504
|
function attemptYouTubeMusicTransportClick() {
|
|
3002
3505
|
if (!isYouTubeMusic || !(wantsNext || wantsPrevious || wantsPause || wantsResume)) {
|
|
3003
3506
|
return null;
|
|
@@ -3045,6 +3548,97 @@ function attemptYouTubeMusicSearchResultClick() {
|
|
|
3045
3548
|
return null;
|
|
3046
3549
|
}
|
|
3047
3550
|
|
|
3551
|
+
const primaryCardButtons = Array.from(document.querySelectorAll("button, [role='button'], a[href*='watch?v=']"))
|
|
3552
|
+
.filter((node) => node instanceof HTMLElement || node instanceof HTMLAnchorElement)
|
|
3553
|
+
.filter((node) => isVisible(node))
|
|
3554
|
+
.filter((node) => !(node.closest("ytmusic-player-bar")))
|
|
3555
|
+
.map((node) => {
|
|
3556
|
+
const element = node;
|
|
3557
|
+
const rect = element.getBoundingClientRect();
|
|
3558
|
+
const primaryCard = pickYouTubeMusicPrimaryCard(element);
|
|
3559
|
+
const primaryCardRect = primaryCard ? primaryCard.getBoundingClientRect() : rect;
|
|
3560
|
+
const label = normalize([
|
|
3561
|
+
deriveText(element),
|
|
3562
|
+
element.getAttribute("aria-label"),
|
|
3563
|
+
element.getAttribute("title"),
|
|
3564
|
+
].filter(Boolean).join(" "));
|
|
3565
|
+
const cardText = normalize(primaryCard ? deriveText(primaryCard) : deriveText(element.parentElement || element));
|
|
3566
|
+
const isPlayButton = /\b(play|reproduzir|tocar)\b/.test(label);
|
|
3567
|
+
const buttonCenterX = rect.left + (rect.width / 2);
|
|
3568
|
+
const cardCenterX = primaryCardRect.left + (primaryCardRect.width / 2);
|
|
3569
|
+
let score = 0;
|
|
3570
|
+
|
|
3571
|
+
if (!isPlayButton) {
|
|
3572
|
+
return null;
|
|
3573
|
+
}
|
|
3574
|
+
if (buttonCenterX > (window.innerWidth * 0.44)) {
|
|
3575
|
+
return null;
|
|
3576
|
+
}
|
|
3577
|
+
if (rect.width < 110 || rect.height < 36) {
|
|
3578
|
+
return null;
|
|
3579
|
+
}
|
|
3580
|
+
if (primaryCardRect.left > (window.innerWidth * 0.16)) {
|
|
3581
|
+
return null;
|
|
3582
|
+
}
|
|
3583
|
+
if (cardCenterX > (window.innerWidth * 0.36)) {
|
|
3584
|
+
return null;
|
|
3585
|
+
}
|
|
3586
|
+
if (primaryCardRect.width < (window.innerWidth * 0.28)) {
|
|
3587
|
+
return null;
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
for (const phrase of quotedPhrases) {
|
|
3591
|
+
if (!phrase) continue;
|
|
3592
|
+
if (cardText.includes(phrase) || label.includes(phrase)) score += 220;
|
|
3593
|
+
}
|
|
3594
|
+
|
|
3595
|
+
for (const token of tokens) {
|
|
3596
|
+
if (cardText.includes(token)) score += 30;
|
|
3597
|
+
else if (label.includes(token)) score += 16;
|
|
3598
|
+
}
|
|
3599
|
+
|
|
3600
|
+
if (tokens.length > 1) {
|
|
3601
|
+
const matchCount = tokens.filter((token) => cardText.includes(token)).length;
|
|
3602
|
+
if (matchCount >= Math.max(2, Math.ceil(tokens.length * 0.5))) score += 120;
|
|
3603
|
+
if (matchCount === tokens.length) score += 80;
|
|
3604
|
+
}
|
|
3605
|
+
|
|
3606
|
+
if ((primaryCardRect.left + (primaryCardRect.width / 2)) < (window.innerWidth * 0.48)) score += 320;
|
|
3607
|
+
if (primaryCardRect.left <= window.innerWidth * 0.18) score += 140;
|
|
3608
|
+
if (primaryCardRect.width >= window.innerWidth * 0.38) score += 120;
|
|
3609
|
+
if ((primaryCardRect.left + primaryCardRect.width) <= window.innerWidth * 0.7) score += 160;
|
|
3610
|
+
if (buttonCenterX < (window.innerWidth * 0.34)) score += 220;
|
|
3611
|
+
if (buttonCenterX < (window.innerWidth * 0.4)) score += 120;
|
|
3612
|
+
if (rect.top < (window.innerHeight * 0.7)) score += 90;
|
|
3613
|
+
if (rect.width >= 120) score += 36;
|
|
3614
|
+
if (rect.height >= 44) score += 18;
|
|
3615
|
+
if (cardText.includes("mais do youtube")) score -= 320;
|
|
3616
|
+
if (primaryCardRect.left > window.innerWidth * 0.45) score -= 260;
|
|
3617
|
+
if (buttonCenterX > window.innerWidth * 0.42) score -= 360;
|
|
3618
|
+
score += Math.max(0, 40 - Math.round(rect.top / 20));
|
|
3619
|
+
|
|
3620
|
+
return score > 0 ? {
|
|
3621
|
+
node: element,
|
|
3622
|
+
score,
|
|
3623
|
+
label,
|
|
3624
|
+
matchedText: primaryCard ? deriveText(primaryCard) : deriveText(element),
|
|
3625
|
+
} : null;
|
|
3626
|
+
})
|
|
3627
|
+
.filter(Boolean)
|
|
3628
|
+
.sort((left, right) => right.score - left.score);
|
|
3629
|
+
|
|
3630
|
+
if (primaryCardButtons.length) {
|
|
3631
|
+
const winner = primaryCardButtons[0];
|
|
3632
|
+
return clickElement(
|
|
3633
|
+
winner.node,
|
|
3634
|
+
"safari_dom_ytmusic_primary_card",
|
|
3635
|
+
winner.matchedText || winner.label,
|
|
3636
|
+
"",
|
|
3637
|
+
winner.score,
|
|
3638
|
+
primaryCardButtons.length,
|
|
3639
|
+
);
|
|
3640
|
+
}
|
|
3641
|
+
|
|
3048
3642
|
const rows = Array.from(document.querySelectorAll("ytmusic-responsive-list-item-renderer"))
|
|
3049
3643
|
.filter((node) => node instanceof HTMLElement)
|
|
3050
3644
|
.filter((node) => isVisible(node));
|
|
@@ -3137,6 +3731,215 @@ if (ytmResult) {
|
|
|
3137
3731
|
return ytmResult;
|
|
3138
3732
|
}
|
|
3139
3733
|
|
|
3734
|
+
function attemptSpotifyTrackPagePlayClick() {
|
|
3735
|
+
if (!location.hostname.includes("open.spotify.com")) {
|
|
3736
|
+
return null;
|
|
3737
|
+
}
|
|
3738
|
+
if (!location.pathname.includes("/track/")) {
|
|
3739
|
+
return null;
|
|
3740
|
+
}
|
|
3741
|
+
|
|
3742
|
+
const rankedButtons = Array.from(document.querySelectorAll(
|
|
3743
|
+
"main [data-testid='play-button'], main button[aria-label], main button[title], [data-testid='entityHeader'] button, main button"
|
|
3744
|
+
))
|
|
3745
|
+
.filter((node) => node instanceof HTMLElement)
|
|
3746
|
+
.filter((node) => isVisible(node))
|
|
3747
|
+
.filter((node) => !(node.closest("footer")))
|
|
3748
|
+
.map((node, index) => {
|
|
3749
|
+
const rect = node.getBoundingClientRect();
|
|
3750
|
+
const label = normalize([
|
|
3751
|
+
deriveText(node),
|
|
3752
|
+
node.getAttribute("aria-label"),
|
|
3753
|
+
node.getAttribute("title"),
|
|
3754
|
+
node.getAttribute("data-testid"),
|
|
3755
|
+
].filter(Boolean).join(" "));
|
|
3756
|
+
const background = normalize(window.getComputedStyle(node).backgroundColor || "");
|
|
3757
|
+
let score = 0;
|
|
3758
|
+
|
|
3759
|
+
if (!/play|tocar|reproduzir/.test(label)) {
|
|
3760
|
+
return null;
|
|
3761
|
+
}
|
|
3762
|
+
|
|
3763
|
+
if (node.getAttribute("data-testid") === "play-button") score += 220;
|
|
3764
|
+
if (label.includes("play")) score += 100;
|
|
3765
|
+
if (background.includes("29, 185, 84") || background.includes("30, 215, 96")) score += 180;
|
|
3766
|
+
if (rect.top <= window.innerHeight * 0.72) score += 90;
|
|
3767
|
+
if (rect.top <= window.innerHeight * 0.56) score += 70;
|
|
3768
|
+
if (rect.left <= window.innerWidth * 0.4) score += 80;
|
|
3769
|
+
if (rect.width >= 44) score += 30;
|
|
3770
|
+
if (rect.height >= 44) score += 30;
|
|
3771
|
+
if (node.closest("[data-testid='entityHeader'], main")) score += 60;
|
|
3772
|
+
score += Math.max(0, 18 - index);
|
|
3773
|
+
|
|
3774
|
+
return score > 0 ? { node, label, score } : null;
|
|
3775
|
+
})
|
|
3776
|
+
.filter(Boolean)
|
|
3777
|
+
.sort((left, right) => right.score - left.score);
|
|
3778
|
+
|
|
3779
|
+
if (!rankedButtons.length) {
|
|
3780
|
+
return null;
|
|
3781
|
+
}
|
|
3782
|
+
|
|
3783
|
+
const winner = rankedButtons[0];
|
|
3784
|
+
return clickElement(
|
|
3785
|
+
winner.node,
|
|
3786
|
+
"safari_dom_spotify_track_page_play",
|
|
3787
|
+
winner.label,
|
|
3788
|
+
location.href || "",
|
|
3789
|
+
winner.score,
|
|
3790
|
+
rankedButtons.length,
|
|
3791
|
+
);
|
|
3792
|
+
}
|
|
3793
|
+
|
|
3794
|
+
const spotifyTrackPagePlay = attemptSpotifyTrackPagePlayClick();
|
|
3795
|
+
if (spotifyTrackPagePlay) {
|
|
3796
|
+
return spotifyTrackPagePlay;
|
|
3797
|
+
}
|
|
3798
|
+
|
|
3799
|
+
function attemptSpotifySearchResultClick() {
|
|
3800
|
+
if (!location.hostname.includes("open.spotify.com")) {
|
|
3801
|
+
return null;
|
|
3802
|
+
}
|
|
3803
|
+
if (!quotedPhrases.length && !tokens.length) {
|
|
3804
|
+
return null;
|
|
3805
|
+
}
|
|
3806
|
+
|
|
3807
|
+
const rows = Array.from(document.querySelectorAll(
|
|
3808
|
+
"[data-testid='tracklist-row'], [role='row']"
|
|
3809
|
+
))
|
|
3810
|
+
.filter((node) => node instanceof HTMLElement)
|
|
3811
|
+
.filter((node) => isVisible(node));
|
|
3812
|
+
|
|
3813
|
+
const rankedRows = rows
|
|
3814
|
+
.map((row, index) => {
|
|
3815
|
+
const titleNode = row.querySelector(
|
|
3816
|
+
"[data-testid='internal-track-link'], a[href*='/track/'], [aria-colindex='2'] a[href*='/track/']"
|
|
3817
|
+
);
|
|
3818
|
+
const navigationTarget = titleNode instanceof HTMLAnchorElement || titleNode instanceof HTMLElement
|
|
3819
|
+
? titleNode
|
|
3820
|
+
: null;
|
|
3821
|
+
const titleText = String((titleNode && titleNode.textContent) || "").trim();
|
|
3822
|
+
const rowText = deriveText(row);
|
|
3823
|
+
const normalizedTitle = normalize(titleText);
|
|
3824
|
+
const normalizedRow = normalize(rowText);
|
|
3825
|
+
let score = 0;
|
|
3826
|
+
|
|
3827
|
+
for (const phrase of quotedPhrases) {
|
|
3828
|
+
if (!phrase) continue;
|
|
3829
|
+
if (normalizedTitle.includes(phrase)) score += 180;
|
|
3830
|
+
else if (normalizedRow.includes(phrase)) score += 110;
|
|
3831
|
+
}
|
|
3832
|
+
|
|
3833
|
+
for (const token of tokens) {
|
|
3834
|
+
if (normalizedTitle.includes(token)) score += 26;
|
|
3835
|
+
else if (normalizedRow.includes(token)) score += 10;
|
|
3836
|
+
}
|
|
3837
|
+
|
|
3838
|
+
const titleMatches = tokens.filter((token) => normalizedTitle.includes(token)).length;
|
|
3839
|
+
if (tokens.length > 1 && titleMatches >= Math.max(2, Math.ceil(tokens.length * 0.5))) score += 90;
|
|
3840
|
+
if (titleMatches === tokens.length && tokens.length > 0) score += 70;
|
|
3841
|
+
if (index < 4) score += Math.max(0, 24 - (index * 5));
|
|
3842
|
+
|
|
3843
|
+
const playCandidate = Array.from(row.querySelectorAll("button, [role='button'], a[href*='/track/']"))
|
|
3844
|
+
.filter((candidate) => candidate instanceof HTMLElement || candidate instanceof HTMLAnchorElement)
|
|
3845
|
+
.filter((candidate) => isVisible(candidate))
|
|
3846
|
+
.map((candidate) => {
|
|
3847
|
+
const label = normalize([
|
|
3848
|
+
deriveText(candidate),
|
|
3849
|
+
candidate.getAttribute("aria-label"),
|
|
3850
|
+
candidate.getAttribute("title"),
|
|
3851
|
+
].filter(Boolean).join(" "));
|
|
3852
|
+
let candidateScore = 0;
|
|
3853
|
+
if (/play|tocar|reproduzir/.test(label)) candidateScore += 40;
|
|
3854
|
+
if (candidate instanceof HTMLAnchorElement && normalize(candidate.href).includes("/track/")) candidateScore += 18;
|
|
3855
|
+
return { candidate, candidateScore };
|
|
3856
|
+
})
|
|
3857
|
+
.sort((left, right) => right.candidateScore - left.candidateScore);
|
|
3858
|
+
|
|
3859
|
+
if (wantsSpotifyTrackPageOpen && !navigationTarget) {
|
|
3860
|
+
return null;
|
|
3861
|
+
}
|
|
3862
|
+
|
|
3863
|
+
return score > 0 ? {
|
|
3864
|
+
row,
|
|
3865
|
+
titleText,
|
|
3866
|
+
href: titleNode instanceof HTMLAnchorElement ? titleNode.href : "",
|
|
3867
|
+
score: score + (playCandidate[0]?.candidateScore || 0),
|
|
3868
|
+
target: wantsSpotifyTrackPageOpen
|
|
3869
|
+
? navigationTarget
|
|
3870
|
+
: (playCandidate[0]?.candidate || navigationTarget || row),
|
|
3871
|
+
} : null;
|
|
3872
|
+
})
|
|
3873
|
+
.filter(Boolean)
|
|
3874
|
+
.sort((left, right) => right.score - left.score);
|
|
3875
|
+
|
|
3876
|
+
if (!rankedRows.length) {
|
|
3877
|
+
return null;
|
|
3878
|
+
}
|
|
3879
|
+
|
|
3880
|
+
const winner = rankedRows[0];
|
|
3881
|
+
const spotifyRow = winner.row instanceof HTMLElement ? winner.row : null;
|
|
3882
|
+
const titleCandidate = spotifyRow?.querySelector(
|
|
3883
|
+
"[data-testid='internal-track-link'], a[href*='/track/'], [aria-colindex='2'] a[href*='/track/']"
|
|
3884
|
+
);
|
|
3885
|
+
const explicitPlayButton = spotifyRow
|
|
3886
|
+
? Array.from(spotifyRow.querySelectorAll("button, [role='button']"))
|
|
3887
|
+
.filter((node) => node instanceof HTMLElement)
|
|
3888
|
+
.filter((node) => isVisible(node))
|
|
3889
|
+
.find((node) => {
|
|
3890
|
+
const label = normalize([
|
|
3891
|
+
deriveText(node),
|
|
3892
|
+
node.getAttribute("aria-label"),
|
|
3893
|
+
node.getAttribute("title"),
|
|
3894
|
+
].filter(Boolean).join(" "));
|
|
3895
|
+
return /\b(play|tocar|reproduzir)\b/.test(label);
|
|
3896
|
+
})
|
|
3897
|
+
: null;
|
|
3898
|
+
|
|
3899
|
+
const clickTarget = wantsSpotifyTrackPageOpen
|
|
3900
|
+
? (titleCandidate || winner.target)
|
|
3901
|
+
: (explicitPlayButton || titleCandidate || winner.target);
|
|
3902
|
+
const clicked = clickElement(
|
|
3903
|
+
clickTarget,
|
|
3904
|
+
wantsSpotifyTrackPageOpen
|
|
3905
|
+
? "safari_dom_spotify_track_link"
|
|
3906
|
+
: (explicitPlayButton ? "safari_dom_spotify_play_button" : "safari_dom_spotify_result"),
|
|
3907
|
+
winner.titleText || deriveText(winner.row),
|
|
3908
|
+
winner.href || "",
|
|
3909
|
+
winner.score,
|
|
3910
|
+
rankedRows.length,
|
|
3911
|
+
);
|
|
3912
|
+
if (!clicked) {
|
|
3913
|
+
return null;
|
|
3914
|
+
}
|
|
3915
|
+
if (spotifyRow instanceof HTMLElement && !explicitPlayButton && !wantsSpotifyTrackPageOpen) {
|
|
3916
|
+
const rect = spotifyRow.getBoundingClientRect();
|
|
3917
|
+
spotifyRow.dispatchEvent(new MouseEvent("dblclick", {
|
|
3918
|
+
bubbles: true,
|
|
3919
|
+
cancelable: true,
|
|
3920
|
+
view: window,
|
|
3921
|
+
clientX: rect.left + (rect.width / 2),
|
|
3922
|
+
clientY: rect.top + (rect.height / 2),
|
|
3923
|
+
}));
|
|
3924
|
+
}
|
|
3925
|
+
return clicked;
|
|
3926
|
+
}
|
|
3927
|
+
|
|
3928
|
+
const spotifyResult = attemptSpotifySearchResultClick();
|
|
3929
|
+
if (spotifyResult) {
|
|
3930
|
+
return spotifyResult;
|
|
3931
|
+
}
|
|
3932
|
+
|
|
3933
|
+
if (location.hostname.includes("open.spotify.com") && (wantsSpotifyTrackPageOpen || wantsSpotifyTrackPagePlay)) {
|
|
3934
|
+
return {
|
|
3935
|
+
clicked: false,
|
|
3936
|
+
reason: wantsSpotifyTrackPageOpen
|
|
3937
|
+
? "Nenhuma faixa visivel do Spotify Web combinou com a busca atual; nao vou clicar em abas, playlists ou albuns por fallback."
|
|
3938
|
+
: "O Spotify Web nao mostrou um botao principal de reproduzir confiavel; nao vou clicar em elementos genericos por fallback.",
|
|
3939
|
+
strategy: "safari_dom_spotify_no_fallback",
|
|
3940
|
+
};
|
|
3941
|
+
}
|
|
3942
|
+
|
|
3140
3943
|
function scoreCandidate(element, rank) {
|
|
3141
3944
|
const text = deriveText(element);
|
|
3142
3945
|
const href = element instanceof HTMLAnchorElement
|
|
@@ -3257,7 +4060,7 @@ tell application "Safari"
|
|
|
3257
4060
|
activate
|
|
3258
4061
|
if (count of windows) = 0 then error "Safari nao possui janelas abertas."
|
|
3259
4062
|
delay 1
|
|
3260
|
-
set jsCode to "(function(){const title=document.title||'';const url=location.href||'';const text=((document.body&&document.body.innerText)||'').trim().slice(0,12000);const playerButton=document.querySelector('ytmusic-player-bar #play-pause-button, ytmusic-player-bar tp-yt-paper-icon-button#play-pause-button, ytmusic-player-bar tp-yt-paper-icon-button.play-pause-button');
|
|
4063
|
+
set jsCode to "(function(){const title=document.title||'';const url=location.href||'';const text=((document.body&&document.body.innerText)||'').trim().slice(0,12000);const isYouTubeMusic=location.hostname.includes('music.youtube.com');const isSpotify=location.hostname.includes('open.spotify.com');let playerButton=null;let playerTitle='';let playerState='';if(isYouTubeMusic){playerButton=document.querySelector('ytmusic-player-bar #play-pause-button, ytmusic-player-bar tp-yt-paper-icon-button#play-pause-button, ytmusic-player-bar tp-yt-paper-icon-button.play-pause-button');playerTitle=(Array.from(document.querySelectorAll('ytmusic-player-bar .title, ytmusic-player-bar .content-info-wrapper .title, ytmusic-player-bar [slot=title]')).map((node)=>((node&&node.textContent)||'').trim()).find(Boolean))||'';playerState=(playerButton&&((playerButton.getAttribute('title')||playerButton.getAttribute('aria-label')||playerButton.textContent)||'').trim())||'';}else if(isSpotify){const visible=(node)=>{if(!(node instanceof Element))return false;const rect=node.getBoundingClientRect();if(rect.width<4||rect.height<4)return false;const style=window.getComputedStyle(node);if(style.visibility==='hidden'||style.display==='none'||Number(style.opacity||'1')===0)return false;return rect.bottom>=0&&rect.right>=0&&rect.top<=window.innerHeight&&rect.left<=window.innerWidth;};const spotifyTitleCandidates=Array.from(document.querySelectorAll(\"[data-testid='nowplaying-track-link'], footer a[href*='/track/'], [data-testid='now-playing-widget'] a[href*='/track/'], a[href*='/track/']\")).filter((node)=>visible(node)).map((node)=>({node,text:((node&&node.textContent)||'').trim(),rect:node.getBoundingClientRect()})).filter((entry)=>entry.text).sort((left,right)=>{const leftBottomBias=(left.rect.top>=window.innerHeight*0.72?200:0)+(left.rect.left<=window.innerWidth*0.45?120:0)+left.rect.top;const rightBottomBias=(right.rect.top>=window.innerHeight*0.72?200:0)+(right.rect.left<=window.innerWidth*0.45?120:0)+right.rect.top;return rightBottomBias-leftBottomBias;});playerTitle=(spotifyTitleCandidates[0]&&spotifyTitleCandidates[0].text)||'';playerButton=Array.from(document.querySelectorAll(\"footer button, [data-testid='control-button-playpause'], button[aria-label], button[title]\")).filter((node)=>visible(node)).map((node)=>({node,label:((node.getAttribute('aria-label')||node.getAttribute('title')||node.textContent)||'').trim(),rect:node.getBoundingClientRect()})).filter((entry)=>/play|pause|tocar|pausar|reproduzir/i.test(entry.label)).sort((left,right)=>{const leftScore=(left.rect.top>=window.innerHeight*0.72?200:0)+Math.max(0,200-Math.abs((left.rect.left+left.rect.width/2)-(window.innerWidth/2)));const rightScore=(right.rect.top>=window.innerHeight*0.72?200:0)+Math.max(0,200-Math.abs((right.rect.left+right.rect.width/2)-(window.innerWidth/2)));return rightScore-leftScore;})[0]?.node||null;playerState=(playerButton&&((playerButton.getAttribute('aria-label')||playerButton.getAttribute('title')||playerButton.textContent)||'').trim())||'';}return JSON.stringify({title,url,text,playerTitle,playerState});})();"
|
|
3261
4064
|
set pageJson to do JavaScript jsCode in current tab of front window
|
|
3262
4065
|
end tell
|
|
3263
4066
|
return pageJson
|