@leg3ndy/otto-bridge 0.8.0 → 0.8.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/dist/executors/native_macos.js +870 -32
- package/dist/http.js +6 -0
- package/dist/local_automations.js +559 -0
- package/dist/macos_whatsapp_helper.js +111 -0
- package/dist/runtime.js +8 -0
- package/dist/types.js +1 -1
- package/dist/whatsapp_background.js +107 -0
- package/package.json +1 -1
|
@@ -221,6 +221,136 @@ function descriptionWantsPause(description) {
|
|
|
221
221
|
function descriptionWantsResume(description) {
|
|
222
222
|
return /\b(retoma|retomar|resume|continu[ae]r|despausa|play)\b/.test(normalizeText(description || ""));
|
|
223
223
|
}
|
|
224
|
+
function descriptionWantsSpotifyTrackPageOpen(description) {
|
|
225
|
+
const normalized = normalizeText(description || "");
|
|
226
|
+
return /\bspotify\b/.test(normalized)
|
|
227
|
+
&& /\b(pagina|titulo|nome|link)\b/.test(normalized)
|
|
228
|
+
&& /\b(faixa|musica|track)\b/.test(normalized);
|
|
229
|
+
}
|
|
230
|
+
function descriptionWantsSpotifyTrackPagePlay(description) {
|
|
231
|
+
const normalized = normalizeText(description || "");
|
|
232
|
+
return /\bspotify\b/.test(normalized)
|
|
233
|
+
&& /\b(play|reproduzir|tocar|verde|principal)\b/.test(normalized)
|
|
234
|
+
&& /\b(pagina|faixa|musica|track)\b/.test(normalized);
|
|
235
|
+
}
|
|
236
|
+
function isSpotifySafariDomOnlyStep(description) {
|
|
237
|
+
return descriptionWantsSpotifyTrackPageOpen(description) || descriptionWantsSpotifyTrackPagePlay(description);
|
|
238
|
+
}
|
|
239
|
+
function playerStateLooksActive(playerState) {
|
|
240
|
+
const normalized = normalizeText(playerState || "");
|
|
241
|
+
return normalized.includes("pause") || normalized.includes("pausar");
|
|
242
|
+
}
|
|
243
|
+
function browserStateMatchesSpotifyTrackDescription(state, description) {
|
|
244
|
+
if (!state) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
const normalizedUrl = normalizeComparableUrl(state.url || "");
|
|
248
|
+
if (!normalizedUrl.includes("open.spotify.com/track/")) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
const haystack = [
|
|
252
|
+
state.playerTitle || "",
|
|
253
|
+
state.title || "",
|
|
254
|
+
String(state.text || "").slice(0, 4000),
|
|
255
|
+
].join(" ");
|
|
256
|
+
const normalizedHaystack = normalizeText(haystack);
|
|
257
|
+
if (!normalizedHaystack) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
const quotedPhrases = extractQuotedPhrases(description);
|
|
261
|
+
if (quotedPhrases.some((phrase) => normalizedHaystack.includes(phrase))) {
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
const mediaQueryTokens = extractMediaQueryTokens(description);
|
|
265
|
+
if (!mediaQueryTokens.length) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
const matchCount = countMatchingTokens(haystack, mediaQueryTokens);
|
|
269
|
+
return matchCount >= Math.max(1, Math.ceil(mediaQueryTokens.length * 0.5));
|
|
270
|
+
}
|
|
271
|
+
function spotifyStateContentMatchesDescription(state, description) {
|
|
272
|
+
if (!state) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
const normalizedUrl = normalizeComparableUrl(state.url || "");
|
|
276
|
+
if (!normalizedUrl.includes("open.spotify.com")) {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
const haystack = [
|
|
280
|
+
state.playerTitle || "",
|
|
281
|
+
state.title || "",
|
|
282
|
+
String(state.text || "").slice(0, 4000),
|
|
283
|
+
].join(" ");
|
|
284
|
+
const normalizedHaystack = normalizeText(haystack);
|
|
285
|
+
if (!normalizedHaystack) {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
const quotedPhrases = extractQuotedPhrases(description);
|
|
289
|
+
if (quotedPhrases.some((phrase) => normalizedHaystack.includes(phrase))) {
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
const mediaQueryTokens = extractMediaQueryTokens(description);
|
|
293
|
+
if (!mediaQueryTokens.length) {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
const matchCount = countMatchingTokens(haystack, mediaQueryTokens);
|
|
297
|
+
return matchCount >= Math.max(1, Math.ceil(mediaQueryTokens.length * 0.5));
|
|
298
|
+
}
|
|
299
|
+
function spotifyPlayerAlreadyPlayingMatchesDescription(state, description) {
|
|
300
|
+
if (!state) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
const normalizedUrl = normalizeComparableUrl(state.url || "");
|
|
304
|
+
if (!normalizedUrl.includes("open.spotify.com")) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
if (!playerStateLooksActive(state.playerState || "")) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
const haystack = [
|
|
311
|
+
state.playerTitle || "",
|
|
312
|
+
state.title || "",
|
|
313
|
+
String(state.text || "").slice(0, 4000),
|
|
314
|
+
].join(" ");
|
|
315
|
+
const normalizedHaystack = normalizeText(haystack);
|
|
316
|
+
if (!normalizedHaystack) {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
const quotedPhrases = extractQuotedPhrases(description);
|
|
320
|
+
if (quotedPhrases.some((phrase) => normalizedHaystack.includes(phrase))) {
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
const mediaQueryTokens = extractMediaQueryTokens(description);
|
|
324
|
+
if (!mediaQueryTokens.length) {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
const matchCount = countMatchingTokens(haystack, mediaQueryTokens);
|
|
328
|
+
return matchCount >= Math.max(1, Math.ceil(mediaQueryTokens.length * 0.5));
|
|
329
|
+
}
|
|
330
|
+
function spotifyTrackAlreadyPlayingMatchesDescription(state, description) {
|
|
331
|
+
return browserStateMatchesSpotifyTrackDescription(state, description)
|
|
332
|
+
&& spotifyPlayerAlreadyPlayingMatchesDescription(state, description);
|
|
333
|
+
}
|
|
334
|
+
function spotifyTrackPageLooksPlaying(state, description) {
|
|
335
|
+
return browserStateMatchesSpotifyTrackDescription(state, description)
|
|
336
|
+
&& playerStateLooksActive(state?.playerState || "");
|
|
337
|
+
}
|
|
338
|
+
function spotifyDescriptionsLikelyMatch(left, right) {
|
|
339
|
+
const leftPhrases = new Set(extractQuotedPhrases(left));
|
|
340
|
+
const rightPhrases = new Set(extractQuotedPhrases(right));
|
|
341
|
+
for (const phrase of leftPhrases) {
|
|
342
|
+
if (rightPhrases.has(phrase)) {
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const leftTokens = extractMediaQueryTokens(left);
|
|
347
|
+
const rightTokens = new Set(extractMediaQueryTokens(right));
|
|
348
|
+
if (!leftTokens.length || !rightTokens.size) {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
const overlap = leftTokens.filter((token) => rightTokens.has(token)).length;
|
|
352
|
+
return overlap >= Math.max(1, Math.ceil(Math.min(leftTokens.length, rightTokens.size) * 0.5));
|
|
353
|
+
}
|
|
224
354
|
function extractNativeMediaTransportCommand(description) {
|
|
225
355
|
const normalizedDescription = normalizeText(description || "");
|
|
226
356
|
if (!normalizedDescription
|
|
@@ -473,6 +603,10 @@ function urlHostname(url) {
|
|
|
473
603
|
return null;
|
|
474
604
|
}
|
|
475
605
|
}
|
|
606
|
+
function isSpotifySearchUrl(url) {
|
|
607
|
+
const normalized = normalizeComparableUrl(normalizeUrl(url));
|
|
608
|
+
return normalized.includes("open.spotify.com/search/");
|
|
609
|
+
}
|
|
476
610
|
function uniqueStrings(values) {
|
|
477
611
|
const seen = new Set();
|
|
478
612
|
const result = [];
|
|
@@ -971,6 +1105,9 @@ export class NativeMacOSJobExecutor {
|
|
|
971
1105
|
lastActiveApp = null;
|
|
972
1106
|
lastVisualTargetDescription = null;
|
|
973
1107
|
lastVisualTargetApp = null;
|
|
1108
|
+
lastSatisfiedSpotifyDescription = null;
|
|
1109
|
+
lastSatisfiedSpotifyConfirmedPlaying = false;
|
|
1110
|
+
lastSatisfiedSpotifyAt = 0;
|
|
974
1111
|
whatsappBackgroundBrowser = null;
|
|
975
1112
|
whatsappRuntimeMonitor = null;
|
|
976
1113
|
constructor(bridgeConfig) {
|
|
@@ -987,6 +1124,9 @@ export class NativeMacOSJobExecutor {
|
|
|
987
1124
|
if (actions.length === 0) {
|
|
988
1125
|
throw new Error("Otto Bridge native-macos could not derive a supported local action from this request");
|
|
989
1126
|
}
|
|
1127
|
+
this.lastSatisfiedSpotifyDescription = null;
|
|
1128
|
+
this.lastSatisfiedSpotifyConfirmedPlaying = false;
|
|
1129
|
+
this.lastSatisfiedSpotifyAt = 0;
|
|
990
1130
|
await reporter.accepted();
|
|
991
1131
|
const confirmation = extractConfirmationOptions(job, actions);
|
|
992
1132
|
if (confirmation.required) {
|
|
@@ -1025,7 +1165,7 @@ export class NativeMacOSJobExecutor {
|
|
|
1025
1165
|
}
|
|
1026
1166
|
if (action.type === "press_shortcut") {
|
|
1027
1167
|
await reporter.progress(progressPercent, `Enviando atalho ${action.shortcut}`);
|
|
1028
|
-
await this.pressShortcut(action.shortcut);
|
|
1168
|
+
const shortcutResult = await this.pressShortcut(action.shortcut);
|
|
1029
1169
|
if (action.shortcut.startsWith("media_")) {
|
|
1030
1170
|
const mediaSummaryMap = {
|
|
1031
1171
|
media_next: "Acionei o comando de próxima mídia no macOS.",
|
|
@@ -1035,7 +1175,14 @@ export class NativeMacOSJobExecutor {
|
|
|
1035
1175
|
media_play: "Acionei o comando de reproduzir mídia no macOS.",
|
|
1036
1176
|
media_play_pause: "Acionei o comando de play/pause de mídia no macOS.",
|
|
1037
1177
|
};
|
|
1038
|
-
|
|
1178
|
+
const mediaSkippedSummaryMap = {
|
|
1179
|
+
media_pause: "Nenhuma mídia estava tocando no macOS, então o pause global foi pulado.",
|
|
1180
|
+
media_resume: "Já havia mídia tocando no macOS, então o comando de retomar foi pulado.",
|
|
1181
|
+
media_play: "Já havia mídia tocando no macOS, então o comando de reproduzir foi pulado.",
|
|
1182
|
+
};
|
|
1183
|
+
completionNotes.push(shortcutResult.performed
|
|
1184
|
+
? (mediaSummaryMap[action.shortcut] || `Acionei ${action.shortcut} no macOS.`)
|
|
1185
|
+
: (mediaSkippedSummaryMap[action.shortcut] || shortcutResult.reason || `O atalho ${action.shortcut} foi pulado no macOS.`));
|
|
1039
1186
|
}
|
|
1040
1187
|
continue;
|
|
1041
1188
|
}
|
|
@@ -1239,7 +1386,9 @@ export class NativeMacOSJobExecutor {
|
|
|
1239
1386
|
await reporter.progress(progressPercent, `Trazendo ${action.app} para frente antes do clique`);
|
|
1240
1387
|
await this.focusApp(action.app);
|
|
1241
1388
|
}
|
|
1242
|
-
const targetDescriptions =
|
|
1389
|
+
const targetDescriptions = isSpotifySafariDomOnlyStep(action.description)
|
|
1390
|
+
? [action.description]
|
|
1391
|
+
: uniqueStrings([action.description, ...(action.retry_descriptions || [])]);
|
|
1243
1392
|
let clickSucceeded = false;
|
|
1244
1393
|
let lastFailureReason = "";
|
|
1245
1394
|
for (let attempt = 0; attempt < targetDescriptions.length; attempt += 1) {
|
|
@@ -1247,6 +1396,33 @@ export class NativeMacOSJobExecutor {
|
|
|
1247
1396
|
const initialBrowserState = browserApp
|
|
1248
1397
|
? await this.captureBrowserPageState(browserApp).catch(() => null)
|
|
1249
1398
|
: null;
|
|
1399
|
+
const isSpotifySafariStep = browserApp === "Safari" && isSpotifySafariDomOnlyStep(targetDescription);
|
|
1400
|
+
const verificationPrompt = isSpotifySafariStep ? undefined : action.verification_prompt;
|
|
1401
|
+
if (isSpotifySafariStep) {
|
|
1402
|
+
await reporter.progress(progressPercent, `Tentando concluir ${targetDescription} pelo DOM do Spotify no Safari`);
|
|
1403
|
+
const spotifyDomResult = await this.executeSpotifySafariDomStep(targetDescription, initialBrowserState);
|
|
1404
|
+
if (spotifyDomResult.ok) {
|
|
1405
|
+
this.rememberSatisfiedSpotifyStep(targetDescription, !!spotifyDomResult.confirmedPlaying);
|
|
1406
|
+
this.lastVisualTargetDescription = targetDescription;
|
|
1407
|
+
this.lastVisualTargetApp = browserApp || action.app || this.lastActiveApp;
|
|
1408
|
+
resultPayload.last_click = {
|
|
1409
|
+
strategy: spotifyDomResult.skipped
|
|
1410
|
+
? "spotify_browser_state_skip"
|
|
1411
|
+
: (spotifyDomResult.click?.strategy || "spotify_safari_dom"),
|
|
1412
|
+
matched_text: spotifyDomResult.click?.matchedText || targetDescription,
|
|
1413
|
+
matched_href: spotifyDomResult.click?.matchedHref || spotifyDomResult.afterState?.url || null,
|
|
1414
|
+
score: spotifyDomResult.click?.score || null,
|
|
1415
|
+
total_candidates: spotifyDomResult.click?.totalCandidates || null,
|
|
1416
|
+
};
|
|
1417
|
+
completionNotes.push(spotifyDomResult.skipped
|
|
1418
|
+
? `O Spotify Web ja estava no estado certo para ${targetDescription}, então pulei tentativas extras.`
|
|
1419
|
+
: `Concluí ${targetDescription} diretamente pelo DOM do Spotify Web no Safari.`);
|
|
1420
|
+
clickSucceeded = true;
|
|
1421
|
+
break;
|
|
1422
|
+
}
|
|
1423
|
+
lastFailureReason = spotifyDomResult.reason || `Nao consegui concluir ${targetDescription} pelo DOM do Spotify Web no Safari.`;
|
|
1424
|
+
continue;
|
|
1425
|
+
}
|
|
1250
1426
|
const nativeMediaTransport = extractNativeMediaTransportCommand(targetDescription);
|
|
1251
1427
|
if (nativeMediaTransport) {
|
|
1252
1428
|
await reporter.progress(progressPercent, `Tentando controle de mídia nativo do macOS para ${targetDescription}`);
|
|
@@ -1254,8 +1430,13 @@ export class NativeMacOSJobExecutor {
|
|
|
1254
1430
|
await this.triggerMacOSMediaTransport(nativeMediaTransport);
|
|
1255
1431
|
let validated = false;
|
|
1256
1432
|
let validationReason = "";
|
|
1257
|
-
if (
|
|
1258
|
-
const
|
|
1433
|
+
if (browserApp) {
|
|
1434
|
+
const browserValidation = await this.confirmBrowserClick(browserApp, initialBrowserState, targetDescription, null);
|
|
1435
|
+
validated = browserValidation.ok;
|
|
1436
|
+
validationReason = browserValidation.reason;
|
|
1437
|
+
}
|
|
1438
|
+
if (!validated && verificationPrompt) {
|
|
1439
|
+
const verification = await this.validateVisualClickWithVision(job.job_id, targetDescription, verificationPrompt, progressPercent, reporter, artifacts, "native_media_transport_result");
|
|
1259
1440
|
if (verification.unavailable) {
|
|
1260
1441
|
lastFailureReason = verification.reason;
|
|
1261
1442
|
break;
|
|
@@ -1263,12 +1444,7 @@ export class NativeMacOSJobExecutor {
|
|
|
1263
1444
|
validated = verification.ok;
|
|
1264
1445
|
validationReason = verification.reason;
|
|
1265
1446
|
}
|
|
1266
|
-
else if (browserApp) {
|
|
1267
|
-
const browserValidation = await this.confirmBrowserClick(browserApp, initialBrowserState, targetDescription, null);
|
|
1268
|
-
validated = browserValidation.ok;
|
|
1269
|
-
validationReason = browserValidation.reason;
|
|
1270
|
-
}
|
|
1271
|
-
else {
|
|
1447
|
+
else if (!browserApp) {
|
|
1272
1448
|
validated = true;
|
|
1273
1449
|
}
|
|
1274
1450
|
if (validated) {
|
|
@@ -1293,8 +1469,13 @@ export class NativeMacOSJobExecutor {
|
|
|
1293
1469
|
if (domClick?.clicked) {
|
|
1294
1470
|
let validated = false;
|
|
1295
1471
|
let validationReason = "";
|
|
1296
|
-
if (
|
|
1297
|
-
const
|
|
1472
|
+
if (browserApp) {
|
|
1473
|
+
const browserValidation = await this.confirmBrowserClick(browserApp, initialBrowserState, targetDescription, domClick.matchedHref || null);
|
|
1474
|
+
validated = browserValidation.ok;
|
|
1475
|
+
validationReason = browserValidation.reason;
|
|
1476
|
+
}
|
|
1477
|
+
if (!validated && verificationPrompt) {
|
|
1478
|
+
const verification = await this.validateVisualClickWithVision(job.job_id, targetDescription, verificationPrompt, progressPercent, reporter, artifacts, "dom_click_result");
|
|
1298
1479
|
if (verification.unavailable) {
|
|
1299
1480
|
lastFailureReason = verification.reason;
|
|
1300
1481
|
break;
|
|
@@ -1302,10 +1483,8 @@ export class NativeMacOSJobExecutor {
|
|
|
1302
1483
|
validated = verification.ok;
|
|
1303
1484
|
validationReason = verification.reason;
|
|
1304
1485
|
}
|
|
1305
|
-
else {
|
|
1306
|
-
|
|
1307
|
-
validated = browserValidation.ok;
|
|
1308
|
-
validationReason = browserValidation.reason;
|
|
1486
|
+
else if (!browserApp) {
|
|
1487
|
+
validated = true;
|
|
1309
1488
|
}
|
|
1310
1489
|
if (validated) {
|
|
1311
1490
|
this.lastVisualTargetDescription = targetDescription;
|
|
@@ -1336,8 +1515,13 @@ export class NativeMacOSJobExecutor {
|
|
|
1336
1515
|
if (ocrClick.clicked) {
|
|
1337
1516
|
let validated = false;
|
|
1338
1517
|
let validationReason = "";
|
|
1339
|
-
if (
|
|
1340
|
-
const
|
|
1518
|
+
if (browserApp) {
|
|
1519
|
+
const browserValidation = await this.confirmBrowserClick(browserApp, visualBeforeState, targetDescription, null);
|
|
1520
|
+
validated = browserValidation.ok;
|
|
1521
|
+
validationReason = browserValidation.reason;
|
|
1522
|
+
}
|
|
1523
|
+
if (!validated && verificationPrompt) {
|
|
1524
|
+
const verification = await this.validateVisualClickWithVision(job.job_id, targetDescription, verificationPrompt, progressPercent, reporter, artifacts, "local_ocr_click_result");
|
|
1341
1525
|
if (verification.unavailable) {
|
|
1342
1526
|
lastFailureReason = verification.reason;
|
|
1343
1527
|
break;
|
|
@@ -1345,12 +1529,7 @@ export class NativeMacOSJobExecutor {
|
|
|
1345
1529
|
validated = verification.ok;
|
|
1346
1530
|
validationReason = verification.reason;
|
|
1347
1531
|
}
|
|
1348
|
-
else if (browserApp) {
|
|
1349
|
-
const browserValidation = await this.confirmBrowserClick(browserApp, visualBeforeState, targetDescription, null);
|
|
1350
|
-
validated = browserValidation.ok;
|
|
1351
|
-
validationReason = browserValidation.reason;
|
|
1352
|
-
}
|
|
1353
|
-
else {
|
|
1532
|
+
else if (!browserApp) {
|
|
1354
1533
|
validated = true;
|
|
1355
1534
|
}
|
|
1356
1535
|
if (validated) {
|
|
@@ -1422,8 +1601,8 @@ export class NativeMacOSJobExecutor {
|
|
|
1422
1601
|
y: scaledY,
|
|
1423
1602
|
strategy: "visual_locator",
|
|
1424
1603
|
};
|
|
1425
|
-
if (
|
|
1426
|
-
const verification = await this.validateVisualClickWithVision(job.job_id, targetDescription,
|
|
1604
|
+
if (verificationPrompt) {
|
|
1605
|
+
const verification = await this.validateVisualClickWithVision(job.job_id, targetDescription, verificationPrompt, progressPercent, reporter, artifacts, "visual_click_result");
|
|
1427
1606
|
if (verification.unavailable) {
|
|
1428
1607
|
lastFailureReason = verification.reason;
|
|
1429
1608
|
break;
|
|
@@ -1528,6 +1707,9 @@ export class NativeMacOSJobExecutor {
|
|
|
1528
1707
|
await this.runCommand("open", ["-a", app, url]);
|
|
1529
1708
|
}
|
|
1530
1709
|
await this.focusApp(app);
|
|
1710
|
+
if (isSpotifySearchUrl(url)) {
|
|
1711
|
+
await this.ensureSafariSpotifySearchUrl(url);
|
|
1712
|
+
}
|
|
1531
1713
|
this.lastActiveApp = app;
|
|
1532
1714
|
return;
|
|
1533
1715
|
}
|
|
@@ -1539,6 +1721,48 @@ export class NativeMacOSJobExecutor {
|
|
|
1539
1721
|
}
|
|
1540
1722
|
await this.runCommand("open", [url]);
|
|
1541
1723
|
}
|
|
1724
|
+
async ensureSafariSpotifySearchUrl(url) {
|
|
1725
|
+
const targetUrl = normalizeUrl(url);
|
|
1726
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1727
|
+
await delay(700);
|
|
1728
|
+
const currentUrl = await this.readSafariCurrentUrl().catch(() => "");
|
|
1729
|
+
const normalizedCurrentUrl = normalizeComparableUrl(currentUrl);
|
|
1730
|
+
if (!normalizedCurrentUrl || !normalizedCurrentUrl.includes("open.spotify.com")) {
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
if (normalizedCurrentUrl.includes("/search/")) {
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
if (!/open\.spotify\.com\/?$/.test(normalizedCurrentUrl)) {
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
await this.setSafariFrontTabUrl(targetUrl).catch(() => undefined);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
async readSafariCurrentUrl() {
|
|
1743
|
+
const script = `
|
|
1744
|
+
tell application "Safari"
|
|
1745
|
+
if (count of windows) = 0 then return ""
|
|
1746
|
+
try
|
|
1747
|
+
return URL of current tab of front window
|
|
1748
|
+
on error
|
|
1749
|
+
return ""
|
|
1750
|
+
end try
|
|
1751
|
+
end tell
|
|
1752
|
+
`;
|
|
1753
|
+
const { stdout } = await this.runCommandCapture("osascript", ["-e", script]);
|
|
1754
|
+
return String(stdout || "").trim();
|
|
1755
|
+
}
|
|
1756
|
+
async setSafariFrontTabUrl(url) {
|
|
1757
|
+
const script = `
|
|
1758
|
+
set targetUrl to "${escapeAppleScript(url)}"
|
|
1759
|
+
tell application "Safari"
|
|
1760
|
+
if (count of windows) = 0 then return
|
|
1761
|
+
set URL of current tab of front window to targetUrl
|
|
1762
|
+
end tell
|
|
1763
|
+
`;
|
|
1764
|
+
await this.runCommand("osascript", ["-e", script]);
|
|
1765
|
+
}
|
|
1542
1766
|
async tryReuseSafariTab(url) {
|
|
1543
1767
|
const targetUrl = normalizeUrl(url);
|
|
1544
1768
|
const targetHost = urlHostname(targetUrl);
|
|
@@ -1648,6 +1872,161 @@ end tell
|
|
|
1648
1872
|
playerState: page.playerState || "",
|
|
1649
1873
|
};
|
|
1650
1874
|
}
|
|
1875
|
+
rememberSatisfiedSpotifyStep(description, confirmedPlaying) {
|
|
1876
|
+
if (!isSpotifySafariDomOnlyStep(description)) {
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
this.lastSatisfiedSpotifyDescription = description;
|
|
1880
|
+
this.lastSatisfiedSpotifyConfirmedPlaying = confirmedPlaying;
|
|
1881
|
+
this.lastSatisfiedSpotifyAt = Date.now();
|
|
1882
|
+
}
|
|
1883
|
+
hasRecentSatisfiedSpotifyPlayback(description, maxAgeMs = 10_000) {
|
|
1884
|
+
if (!this.lastSatisfiedSpotifyConfirmedPlaying || !this.lastSatisfiedSpotifyDescription || !this.lastSatisfiedSpotifyAt) {
|
|
1885
|
+
return false;
|
|
1886
|
+
}
|
|
1887
|
+
if ((Date.now() - this.lastSatisfiedSpotifyAt) > maxAgeMs) {
|
|
1888
|
+
return false;
|
|
1889
|
+
}
|
|
1890
|
+
return spotifyDescriptionsLikelyMatch(this.lastSatisfiedSpotifyDescription, description);
|
|
1891
|
+
}
|
|
1892
|
+
hasRecentSatisfiedSpotifyStep(description, maxAgeMs = 10_000) {
|
|
1893
|
+
if (!this.lastSatisfiedSpotifyDescription || !this.lastSatisfiedSpotifyAt) {
|
|
1894
|
+
return false;
|
|
1895
|
+
}
|
|
1896
|
+
if ((Date.now() - this.lastSatisfiedSpotifyAt) > maxAgeMs) {
|
|
1897
|
+
return false;
|
|
1898
|
+
}
|
|
1899
|
+
return spotifyDescriptionsLikelyMatch(this.lastSatisfiedSpotifyDescription, description);
|
|
1900
|
+
}
|
|
1901
|
+
async executeSpotifySafariDomStep(targetDescription, initialBrowserState) {
|
|
1902
|
+
if (descriptionWantsSpotifyTrackPagePlay(targetDescription)
|
|
1903
|
+
&& this.hasRecentSatisfiedSpotifyStep(targetDescription)
|
|
1904
|
+
&& browserStateMatchesSpotifyTrackDescription(initialBrowserState, targetDescription)) {
|
|
1905
|
+
return {
|
|
1906
|
+
ok: true,
|
|
1907
|
+
skipped: true,
|
|
1908
|
+
afterState: initialBrowserState,
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1911
|
+
if (descriptionWantsSpotifyTrackPagePlay(targetDescription) && this.hasRecentSatisfiedSpotifyPlayback(targetDescription)) {
|
|
1912
|
+
return {
|
|
1913
|
+
ok: true,
|
|
1914
|
+
skipped: true,
|
|
1915
|
+
confirmedPlaying: true,
|
|
1916
|
+
afterState: initialBrowserState,
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
if (spotifyPlayerAlreadyPlayingMatchesDescription(initialBrowserState, targetDescription)) {
|
|
1920
|
+
return {
|
|
1921
|
+
ok: true,
|
|
1922
|
+
skipped: true,
|
|
1923
|
+
confirmedPlaying: true,
|
|
1924
|
+
afterState: initialBrowserState,
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
if (descriptionWantsSpotifyTrackPagePlay(targetDescription)) {
|
|
1928
|
+
const shouldWaitLongerForAutoplay = this.hasRecentSatisfiedSpotifyStep(targetDescription);
|
|
1929
|
+
let settledState = initialBrowserState;
|
|
1930
|
+
for (let attempt = 0; attempt < (shouldWaitLongerForAutoplay ? 6 : 3); attempt += 1) {
|
|
1931
|
+
if (attempt > 0) {
|
|
1932
|
+
await delay(shouldWaitLongerForAutoplay ? 550 : 450);
|
|
1933
|
+
}
|
|
1934
|
+
settledState = await this.captureBrowserPageState("Safari").catch(() => settledState);
|
|
1935
|
+
const playbackState = await this.readGlobalMediaPlaybackState();
|
|
1936
|
+
if ((playbackState === "playing" && spotifyStateContentMatchesDescription(settledState, targetDescription))
|
|
1937
|
+
|| spotifyTrackPageLooksPlaying(settledState, targetDescription)
|
|
1938
|
+
|| spotifyTrackAlreadyPlayingMatchesDescription(settledState, targetDescription)) {
|
|
1939
|
+
return {
|
|
1940
|
+
ok: true,
|
|
1941
|
+
skipped: true,
|
|
1942
|
+
confirmedPlaying: true,
|
|
1943
|
+
afterState: settledState,
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
const alreadyOnRequestedTrack = browserStateMatchesSpotifyTrackDescription(initialBrowserState, targetDescription);
|
|
1948
|
+
if (alreadyOnRequestedTrack) {
|
|
1949
|
+
const playbackState = await this.readGlobalMediaPlaybackState();
|
|
1950
|
+
if (playbackState === "playing"
|
|
1951
|
+
|| spotifyTrackPageLooksPlaying(initialBrowserState, targetDescription)
|
|
1952
|
+
|| spotifyTrackAlreadyPlayingMatchesDescription(initialBrowserState, targetDescription)) {
|
|
1953
|
+
return {
|
|
1954
|
+
ok: true,
|
|
1955
|
+
skipped: true,
|
|
1956
|
+
confirmedPlaying: true,
|
|
1957
|
+
afterState: initialBrowserState,
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
const domClick = await this.trySafariDomClick(targetDescription);
|
|
1963
|
+
let currentState = await this.captureBrowserPageState("Safari").catch(() => initialBrowserState);
|
|
1964
|
+
if (!domClick?.clicked) {
|
|
1965
|
+
return {
|
|
1966
|
+
ok: false,
|
|
1967
|
+
reason: domClick?.reason || `Nao consegui concluir ${targetDescription} pelo DOM do Spotify Web no Safari.`,
|
|
1968
|
+
afterState: currentState,
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
1972
|
+
if (attempt > 0) {
|
|
1973
|
+
await delay(500);
|
|
1974
|
+
}
|
|
1975
|
+
currentState = await this.captureBrowserPageState("Safari").catch(() => currentState);
|
|
1976
|
+
if (descriptionWantsSpotifyTrackPageOpen(targetDescription)) {
|
|
1977
|
+
if (browserStateMatchesSpotifyTrackDescription(currentState, targetDescription)) {
|
|
1978
|
+
return {
|
|
1979
|
+
ok: true,
|
|
1980
|
+
click: domClick,
|
|
1981
|
+
afterState: currentState,
|
|
1982
|
+
};
|
|
1983
|
+
}
|
|
1984
|
+
const playbackState = await this.readGlobalMediaPlaybackState();
|
|
1985
|
+
if (playbackState === "playing" && spotifyStateContentMatchesDescription(currentState, targetDescription)) {
|
|
1986
|
+
return {
|
|
1987
|
+
ok: true,
|
|
1988
|
+
click: domClick,
|
|
1989
|
+
confirmedPlaying: true,
|
|
1990
|
+
afterState: currentState,
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
if (descriptionWantsSpotifyTrackPagePlay(targetDescription)) {
|
|
1995
|
+
if (spotifyPlayerAlreadyPlayingMatchesDescription(currentState, targetDescription)) {
|
|
1996
|
+
return {
|
|
1997
|
+
ok: true,
|
|
1998
|
+
click: domClick,
|
|
1999
|
+
confirmedPlaying: true,
|
|
2000
|
+
afterState: currentState,
|
|
2001
|
+
};
|
|
2002
|
+
}
|
|
2003
|
+
const playbackState = await this.readGlobalMediaPlaybackState();
|
|
2004
|
+
if (playbackState === "playing" && spotifyStateContentMatchesDescription(currentState, targetDescription)) {
|
|
2005
|
+
return {
|
|
2006
|
+
ok: true,
|
|
2007
|
+
click: domClick,
|
|
2008
|
+
confirmedPlaying: true,
|
|
2009
|
+
afterState: currentState,
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
if (descriptionWantsSpotifyTrackPageOpen(targetDescription)) {
|
|
2015
|
+
const matchedHref = normalizeComparableUrl(domClick.matchedHref || "");
|
|
2016
|
+
if (matchedHref.includes("/track/")) {
|
|
2017
|
+
return {
|
|
2018
|
+
ok: true,
|
|
2019
|
+
click: domClick,
|
|
2020
|
+
afterState: currentState,
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
return {
|
|
2025
|
+
ok: false,
|
|
2026
|
+
reason: `O clique em ${targetDescription} nao mudou a pagina do navegador de forma verificavel.`,
|
|
2027
|
+
afterState: currentState,
|
|
2028
|
+
};
|
|
2029
|
+
}
|
|
1651
2030
|
resolveExpectedBrowserHref(rawHref, baseUrl) {
|
|
1652
2031
|
const href = String(rawHref || "").trim();
|
|
1653
2032
|
if (!href) {
|
|
@@ -1688,12 +2067,14 @@ end tell
|
|
|
1688
2067
|
const afterPlayerTitle = normalizeText(after.playerTitle || "");
|
|
1689
2068
|
const beforePlayerState = normalizeText(before?.playerState || "");
|
|
1690
2069
|
const afterPlayerState = normalizeText(after.playerState || "");
|
|
1691
|
-
const playerLooksActive =
|
|
2070
|
+
const playerLooksActive = playerStateLooksActive(after.playerState || "");
|
|
1692
2071
|
const playerLooksPaused = !playerLooksActive && /play|tocar|reproduzir|continuar|retomar|resume/.test(afterPlayerState);
|
|
1693
2072
|
const wantsNext = descriptionWantsNext(targetDescription);
|
|
1694
2073
|
const wantsPrevious = descriptionWantsPrevious(targetDescription);
|
|
1695
2074
|
const wantsPause = descriptionWantsPause(targetDescription);
|
|
1696
2075
|
const wantsResume = descriptionWantsResume(targetDescription);
|
|
2076
|
+
const wantsSpotifyTrackPageOpen = descriptionWantsSpotifyTrackPageOpen(targetDescription);
|
|
2077
|
+
const wantsSpotifyTrackPagePlay = descriptionWantsSpotifyTrackPagePlay(targetDescription);
|
|
1697
2078
|
const mediaQueryTokens = extractMediaQueryTokens(targetDescription);
|
|
1698
2079
|
const mediaMatchCount = countMatchingTokens(after.playerTitle || "", mediaQueryTokens);
|
|
1699
2080
|
if (afterUrl.includes("music.youtube.com")) {
|
|
@@ -1719,6 +2100,51 @@ end tell
|
|
|
1719
2100
|
return true;
|
|
1720
2101
|
}
|
|
1721
2102
|
}
|
|
2103
|
+
if (afterUrl.includes("open.spotify.com")) {
|
|
2104
|
+
if (wantsSpotifyTrackPageOpen && spotifyPlayerAlreadyPlayingMatchesDescription(after, targetDescription)) {
|
|
2105
|
+
return true;
|
|
2106
|
+
}
|
|
2107
|
+
if (wantsSpotifyTrackPagePlay && spotifyTrackAlreadyPlayingMatchesDescription(after, targetDescription)) {
|
|
2108
|
+
return true;
|
|
2109
|
+
}
|
|
2110
|
+
if (wantsSpotifyTrackPageOpen) {
|
|
2111
|
+
const beforeWasTrackPage = beforeUrl.includes("/track/");
|
|
2112
|
+
const afterIsTrackPage = afterUrl.includes("/track/");
|
|
2113
|
+
if (afterIsTrackPage && (!beforeWasTrackPage || beforeUrl !== afterUrl)) {
|
|
2114
|
+
return true;
|
|
2115
|
+
}
|
|
2116
|
+
return spotifyPlayerAlreadyPlayingMatchesDescription(after, targetDescription);
|
|
2117
|
+
}
|
|
2118
|
+
if (wantsPause && beforePlayerState && beforePlayerState !== afterPlayerState && playerLooksPaused) {
|
|
2119
|
+
return true;
|
|
2120
|
+
}
|
|
2121
|
+
if (wantsResume && playerLooksActive && beforePlayerState !== afterPlayerState) {
|
|
2122
|
+
return true;
|
|
2123
|
+
}
|
|
2124
|
+
if ((wantsNext || wantsPrevious) && beforePlayerTitle && afterPlayerTitle && beforePlayerTitle !== afterPlayerTitle) {
|
|
2125
|
+
return true;
|
|
2126
|
+
}
|
|
2127
|
+
if (mediaQueryTokens.length >= 2) {
|
|
2128
|
+
const requiredMatches = Math.max(2, Math.ceil(mediaQueryTokens.length * 0.5));
|
|
2129
|
+
const titleLooksCorrect = mediaMatchCount >= requiredMatches;
|
|
2130
|
+
const playerChanged = ((beforePlayerTitle && afterPlayerTitle && beforePlayerTitle !== afterPlayerTitle)
|
|
2131
|
+
|| (!beforePlayerTitle && !!afterPlayerTitle)
|
|
2132
|
+
|| (beforePlayerState && afterPlayerState && beforePlayerState !== afterPlayerState));
|
|
2133
|
+
if (titleLooksCorrect && (playerChanged || (beforePlayerTitle === afterPlayerTitle && playerLooksActive))) {
|
|
2134
|
+
return true;
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
if (beforePlayerTitle && afterPlayerTitle && beforePlayerTitle !== afterPlayerTitle && playerLooksActive) {
|
|
2138
|
+
return true;
|
|
2139
|
+
}
|
|
2140
|
+
if (!beforePlayerTitle && afterPlayerTitle && playerLooksActive) {
|
|
2141
|
+
return true;
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
const looksLikeMediaAction = wantsNext || wantsPrevious || wantsPause || wantsResume || mediaQueryTokens.length > 0;
|
|
2145
|
+
if (looksLikeMediaAction && (afterUrl.includes("music.youtube.com") || afterUrl.includes("open.spotify.com"))) {
|
|
2146
|
+
return false;
|
|
2147
|
+
}
|
|
1722
2148
|
const beforeTitle = normalizeText(before?.title || "");
|
|
1723
2149
|
const afterTitle = normalizeText(after.title || "");
|
|
1724
2150
|
if (beforeTitle && afterTitle && beforeTitle !== afterTitle) {
|
|
@@ -1739,9 +2165,11 @@ end tell
|
|
|
1739
2165
|
afterState: null,
|
|
1740
2166
|
};
|
|
1741
2167
|
}
|
|
2168
|
+
let lastAfterState = null;
|
|
1742
2169
|
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
1743
2170
|
await delay(attempt === 0 ? 900 : 700);
|
|
1744
2171
|
const afterState = await this.captureBrowserPageState(app).catch(() => null);
|
|
2172
|
+
lastAfterState = afterState;
|
|
1745
2173
|
if (this.didBrowserPageStateChange(before, afterState, targetDescription, matchedHref)) {
|
|
1746
2174
|
return {
|
|
1747
2175
|
ok: true,
|
|
@@ -1753,7 +2181,7 @@ end tell
|
|
|
1753
2181
|
return {
|
|
1754
2182
|
ok: false,
|
|
1755
2183
|
reason: `O clique em ${targetDescription} nao mudou a pagina do navegador de forma verificavel.`,
|
|
1756
|
-
afterState:
|
|
2184
|
+
afterState: lastAfterState,
|
|
1757
2185
|
};
|
|
1758
2186
|
}
|
|
1759
2187
|
async pressShortcut(shortcut) {
|
|
@@ -1775,8 +2203,28 @@ end tell
|
|
|
1775
2203
|
};
|
|
1776
2204
|
const mediaCommand = mediaCommandMap[normalizedShortcut];
|
|
1777
2205
|
if (mediaCommand) {
|
|
2206
|
+
if (normalizedShortcut === "media_pause") {
|
|
2207
|
+
const playbackState = await this.readGlobalMediaPlaybackState();
|
|
2208
|
+
if (playbackState !== "playing") {
|
|
2209
|
+
return {
|
|
2210
|
+
performed: false,
|
|
2211
|
+
reason: playbackState === "unknown"
|
|
2212
|
+
? "O macOS nao confirmou nenhuma mídia tocando com segurança."
|
|
2213
|
+
: "Nenhuma mídia estava tocando no macOS.",
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
if (normalizedShortcut === "media_resume" || normalizedShortcut === "media_play") {
|
|
2218
|
+
const playbackState = await this.readGlobalMediaPlaybackState();
|
|
2219
|
+
if (playbackState === "playing") {
|
|
2220
|
+
return {
|
|
2221
|
+
performed: false,
|
|
2222
|
+
reason: "Já havia mídia tocando no macOS.",
|
|
2223
|
+
};
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
1778
2226
|
await this.triggerMacOSMediaTransport(mediaCommand);
|
|
1779
|
-
return;
|
|
2227
|
+
return { performed: true };
|
|
1780
2228
|
}
|
|
1781
2229
|
const namedKeyCodes = {
|
|
1782
2230
|
return: 36,
|
|
@@ -1796,12 +2244,67 @@ end tell
|
|
|
1796
2244
|
"-e",
|
|
1797
2245
|
`tell application "System Events" to key code ${namedKeyCodes[key]}${usingClause}`,
|
|
1798
2246
|
]);
|
|
1799
|
-
return;
|
|
2247
|
+
return { performed: true };
|
|
1800
2248
|
}
|
|
1801
2249
|
await this.runCommand("osascript", [
|
|
1802
2250
|
"-e",
|
|
1803
2251
|
`tell application "System Events" to keystroke "${escapeAppleScript(key)}"${usingClause}`,
|
|
1804
2252
|
]);
|
|
2253
|
+
return { performed: true };
|
|
2254
|
+
}
|
|
2255
|
+
async readGlobalMediaPlaybackState() {
|
|
2256
|
+
const swiftScript = `
|
|
2257
|
+
import Foundation
|
|
2258
|
+
import Dispatch
|
|
2259
|
+
import Darwin
|
|
2260
|
+
|
|
2261
|
+
typealias MRMediaRemoteGetNowPlayingApplicationIsPlayingFunction = @convention(c) (DispatchQueue, @escaping (Bool) -> Void) -> Void
|
|
2262
|
+
|
|
2263
|
+
guard let handle = dlopen("/System/Library/PrivateFrameworks/MediaRemote.framework/MediaRemote", RTLD_NOW) else {
|
|
2264
|
+
print("unknown")
|
|
2265
|
+
exit(0)
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
guard let symbol = dlsym(handle, "MRMediaRemoteGetNowPlayingApplicationIsPlaying") else {
|
|
2269
|
+
print("unknown")
|
|
2270
|
+
exit(0)
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
let fn = unsafeBitCast(symbol, to: MRMediaRemoteGetNowPlayingApplicationIsPlayingFunction.self)
|
|
2274
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
2275
|
+
var isPlaying: Bool? = nil
|
|
2276
|
+
|
|
2277
|
+
fn(DispatchQueue.global()) { value in
|
|
2278
|
+
isPlaying = value
|
|
2279
|
+
semaphore.signal()
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
if semaphore.wait(timeout: .now() + .milliseconds(800)) == .timedOut {
|
|
2283
|
+
print("unknown")
|
|
2284
|
+
exit(0)
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
print(isPlaying == true ? "playing" : "not_playing")
|
|
2288
|
+
`;
|
|
2289
|
+
try {
|
|
2290
|
+
const { stdout } = await this.runCommandCapture("swift", [
|
|
2291
|
+
"-module-cache-path",
|
|
2292
|
+
path.join(os.tmpdir(), "otto-bridge-swift-modcache"),
|
|
2293
|
+
"-e",
|
|
2294
|
+
swiftScript,
|
|
2295
|
+
]);
|
|
2296
|
+
const state = normalizeText(stdout || "").trim();
|
|
2297
|
+
if (state.includes("not_playing")) {
|
|
2298
|
+
return "not_playing";
|
|
2299
|
+
}
|
|
2300
|
+
if (state.includes("playing")) {
|
|
2301
|
+
return "playing";
|
|
2302
|
+
}
|
|
2303
|
+
return "unknown";
|
|
2304
|
+
}
|
|
2305
|
+
catch {
|
|
2306
|
+
return "unknown";
|
|
2307
|
+
}
|
|
1805
2308
|
}
|
|
1806
2309
|
async triggerMacOSMediaTransport(command) {
|
|
1807
2310
|
const keyTypeMap = {
|
|
@@ -2886,6 +3389,14 @@ const wantsNext = /\\b(proxim[ao]?|next|skip|pular|avanca|avancar)\\b/.test(norm
|
|
|
2886
3389
|
const wantsPrevious = /\\b(anterior|previous|volta[ar]?|back|retorna[ar]?)\\b/.test(normalizedDescription);
|
|
2887
3390
|
const wantsPause = /\\b(pausa|pause|pausar)\\b/.test(normalizedDescription);
|
|
2888
3391
|
const wantsResume = /\\b(retoma|retomar|resume|continu[ae]r|despausa|play)\\b/.test(normalizedDescription);
|
|
3392
|
+
const wantsSpotifyTrackPageOpen = location.hostname.includes("open.spotify.com")
|
|
3393
|
+
&& /\\bspotify\\b/.test(normalizedDescription)
|
|
3394
|
+
&& /\\b(pagina|titulo|nome|link)\\b/.test(normalizedDescription)
|
|
3395
|
+
&& /\\b(faixa|musica|track)\\b/.test(normalizedDescription);
|
|
3396
|
+
const wantsSpotifyTrackPagePlay = location.hostname.includes("open.spotify.com")
|
|
3397
|
+
&& /\\bspotify\\b/.test(normalizedDescription)
|
|
3398
|
+
&& /\\b(play|reproduzir|tocar|verde|principal)\\b/.test(normalizedDescription)
|
|
3399
|
+
&& /\\b(pagina|faixa|musica|track)\\b/.test(normalizedDescription);
|
|
2889
3400
|
const stopWords = new Set([
|
|
2890
3401
|
"o", "a", "os", "as", "um", "uma", "uns", "umas", "de", "da", "do", "das", "dos",
|
|
2891
3402
|
"em", "no", "na", "nos", "nas", "para", "por", "com", "que", "visivel", "visiveis",
|
|
@@ -2995,6 +3506,33 @@ function clickElement(element, strategy, matchedText, matchedHref, score, totalC
|
|
|
2995
3506
|
};
|
|
2996
3507
|
}
|
|
2997
3508
|
|
|
3509
|
+
function pickYouTubeMusicPrimaryCard(element) {
|
|
3510
|
+
let current = element instanceof Element ? element : null;
|
|
3511
|
+
let best = null;
|
|
3512
|
+
let bestScore = -Infinity;
|
|
3513
|
+
while (current && current instanceof HTMLElement && current !== document.body) {
|
|
3514
|
+
const rect = current.getBoundingClientRect();
|
|
3515
|
+
const text = normalize(deriveText(current));
|
|
3516
|
+
let score = 0;
|
|
3517
|
+
if (rect.width >= window.innerWidth * 0.32) score += 40;
|
|
3518
|
+
if (rect.width >= window.innerWidth * 0.42) score += 80;
|
|
3519
|
+
if (rect.left <= window.innerWidth * 0.18) score += 90;
|
|
3520
|
+
if ((rect.left + rect.width) <= window.innerWidth * 0.68) score += 120;
|
|
3521
|
+
if (rect.top <= window.innerHeight * 0.7) score += 40;
|
|
3522
|
+
if (text.includes("mais do youtube")) score -= 260;
|
|
3523
|
+
if (text.includes("playlist")) score -= 40;
|
|
3524
|
+
if (text.includes("podcast")) score -= 40;
|
|
3525
|
+
if (text.includes("musica")) score += 28;
|
|
3526
|
+
if (text.includes("video")) score -= 18;
|
|
3527
|
+
if (score > bestScore) {
|
|
3528
|
+
bestScore = score;
|
|
3529
|
+
best = current;
|
|
3530
|
+
}
|
|
3531
|
+
current = current.parentElement;
|
|
3532
|
+
}
|
|
3533
|
+
return best;
|
|
3534
|
+
}
|
|
3535
|
+
|
|
2998
3536
|
function attemptYouTubeMusicTransportClick() {
|
|
2999
3537
|
if (!isYouTubeMusic || !(wantsNext || wantsPrevious || wantsPause || wantsResume)) {
|
|
3000
3538
|
return null;
|
|
@@ -3042,6 +3580,97 @@ function attemptYouTubeMusicSearchResultClick() {
|
|
|
3042
3580
|
return null;
|
|
3043
3581
|
}
|
|
3044
3582
|
|
|
3583
|
+
const primaryCardButtons = Array.from(document.querySelectorAll("button, [role='button'], a[href*='watch?v=']"))
|
|
3584
|
+
.filter((node) => node instanceof HTMLElement || node instanceof HTMLAnchorElement)
|
|
3585
|
+
.filter((node) => isVisible(node))
|
|
3586
|
+
.filter((node) => !(node.closest("ytmusic-player-bar")))
|
|
3587
|
+
.map((node) => {
|
|
3588
|
+
const element = node;
|
|
3589
|
+
const rect = element.getBoundingClientRect();
|
|
3590
|
+
const primaryCard = pickYouTubeMusicPrimaryCard(element);
|
|
3591
|
+
const primaryCardRect = primaryCard ? primaryCard.getBoundingClientRect() : rect;
|
|
3592
|
+
const label = normalize([
|
|
3593
|
+
deriveText(element),
|
|
3594
|
+
element.getAttribute("aria-label"),
|
|
3595
|
+
element.getAttribute("title"),
|
|
3596
|
+
].filter(Boolean).join(" "));
|
|
3597
|
+
const cardText = normalize(primaryCard ? deriveText(primaryCard) : deriveText(element.parentElement || element));
|
|
3598
|
+
const isPlayButton = /\b(play|reproduzir|tocar)\b/.test(label);
|
|
3599
|
+
const buttonCenterX = rect.left + (rect.width / 2);
|
|
3600
|
+
const cardCenterX = primaryCardRect.left + (primaryCardRect.width / 2);
|
|
3601
|
+
let score = 0;
|
|
3602
|
+
|
|
3603
|
+
if (!isPlayButton) {
|
|
3604
|
+
return null;
|
|
3605
|
+
}
|
|
3606
|
+
if (buttonCenterX > (window.innerWidth * 0.44)) {
|
|
3607
|
+
return null;
|
|
3608
|
+
}
|
|
3609
|
+
if (rect.width < 110 || rect.height < 36) {
|
|
3610
|
+
return null;
|
|
3611
|
+
}
|
|
3612
|
+
if (primaryCardRect.left > (window.innerWidth * 0.16)) {
|
|
3613
|
+
return null;
|
|
3614
|
+
}
|
|
3615
|
+
if (cardCenterX > (window.innerWidth * 0.36)) {
|
|
3616
|
+
return null;
|
|
3617
|
+
}
|
|
3618
|
+
if (primaryCardRect.width < (window.innerWidth * 0.28)) {
|
|
3619
|
+
return null;
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3622
|
+
for (const phrase of quotedPhrases) {
|
|
3623
|
+
if (!phrase) continue;
|
|
3624
|
+
if (cardText.includes(phrase) || label.includes(phrase)) score += 220;
|
|
3625
|
+
}
|
|
3626
|
+
|
|
3627
|
+
for (const token of tokens) {
|
|
3628
|
+
if (cardText.includes(token)) score += 30;
|
|
3629
|
+
else if (label.includes(token)) score += 16;
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3632
|
+
if (tokens.length > 1) {
|
|
3633
|
+
const matchCount = tokens.filter((token) => cardText.includes(token)).length;
|
|
3634
|
+
if (matchCount >= Math.max(2, Math.ceil(tokens.length * 0.5))) score += 120;
|
|
3635
|
+
if (matchCount === tokens.length) score += 80;
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3638
|
+
if ((primaryCardRect.left + (primaryCardRect.width / 2)) < (window.innerWidth * 0.48)) score += 320;
|
|
3639
|
+
if (primaryCardRect.left <= window.innerWidth * 0.18) score += 140;
|
|
3640
|
+
if (primaryCardRect.width >= window.innerWidth * 0.38) score += 120;
|
|
3641
|
+
if ((primaryCardRect.left + primaryCardRect.width) <= window.innerWidth * 0.7) score += 160;
|
|
3642
|
+
if (buttonCenterX < (window.innerWidth * 0.34)) score += 220;
|
|
3643
|
+
if (buttonCenterX < (window.innerWidth * 0.4)) score += 120;
|
|
3644
|
+
if (rect.top < (window.innerHeight * 0.7)) score += 90;
|
|
3645
|
+
if (rect.width >= 120) score += 36;
|
|
3646
|
+
if (rect.height >= 44) score += 18;
|
|
3647
|
+
if (cardText.includes("mais do youtube")) score -= 320;
|
|
3648
|
+
if (primaryCardRect.left > window.innerWidth * 0.45) score -= 260;
|
|
3649
|
+
if (buttonCenterX > window.innerWidth * 0.42) score -= 360;
|
|
3650
|
+
score += Math.max(0, 40 - Math.round(rect.top / 20));
|
|
3651
|
+
|
|
3652
|
+
return score > 0 ? {
|
|
3653
|
+
node: element,
|
|
3654
|
+
score,
|
|
3655
|
+
label,
|
|
3656
|
+
matchedText: primaryCard ? deriveText(primaryCard) : deriveText(element),
|
|
3657
|
+
} : null;
|
|
3658
|
+
})
|
|
3659
|
+
.filter(Boolean)
|
|
3660
|
+
.sort((left, right) => right.score - left.score);
|
|
3661
|
+
|
|
3662
|
+
if (primaryCardButtons.length) {
|
|
3663
|
+
const winner = primaryCardButtons[0];
|
|
3664
|
+
return clickElement(
|
|
3665
|
+
winner.node,
|
|
3666
|
+
"safari_dom_ytmusic_primary_card",
|
|
3667
|
+
winner.matchedText || winner.label,
|
|
3668
|
+
"",
|
|
3669
|
+
winner.score,
|
|
3670
|
+
primaryCardButtons.length,
|
|
3671
|
+
);
|
|
3672
|
+
}
|
|
3673
|
+
|
|
3045
3674
|
const rows = Array.from(document.querySelectorAll("ytmusic-responsive-list-item-renderer"))
|
|
3046
3675
|
.filter((node) => node instanceof HTMLElement)
|
|
3047
3676
|
.filter((node) => isVisible(node));
|
|
@@ -3134,6 +3763,215 @@ if (ytmResult) {
|
|
|
3134
3763
|
return ytmResult;
|
|
3135
3764
|
}
|
|
3136
3765
|
|
|
3766
|
+
function attemptSpotifyTrackPagePlayClick() {
|
|
3767
|
+
if (!location.hostname.includes("open.spotify.com")) {
|
|
3768
|
+
return null;
|
|
3769
|
+
}
|
|
3770
|
+
if (!location.pathname.includes("/track/")) {
|
|
3771
|
+
return null;
|
|
3772
|
+
}
|
|
3773
|
+
|
|
3774
|
+
const rankedButtons = Array.from(document.querySelectorAll(
|
|
3775
|
+
"main [data-testid='play-button'], main button[aria-label], main button[title], [data-testid='entityHeader'] button, main button"
|
|
3776
|
+
))
|
|
3777
|
+
.filter((node) => node instanceof HTMLElement)
|
|
3778
|
+
.filter((node) => isVisible(node))
|
|
3779
|
+
.filter((node) => !(node.closest("footer")))
|
|
3780
|
+
.map((node, index) => {
|
|
3781
|
+
const rect = node.getBoundingClientRect();
|
|
3782
|
+
const label = normalize([
|
|
3783
|
+
deriveText(node),
|
|
3784
|
+
node.getAttribute("aria-label"),
|
|
3785
|
+
node.getAttribute("title"),
|
|
3786
|
+
node.getAttribute("data-testid"),
|
|
3787
|
+
].filter(Boolean).join(" "));
|
|
3788
|
+
const background = normalize(window.getComputedStyle(node).backgroundColor || "");
|
|
3789
|
+
let score = 0;
|
|
3790
|
+
|
|
3791
|
+
if (!/play|tocar|reproduzir/.test(label)) {
|
|
3792
|
+
return null;
|
|
3793
|
+
}
|
|
3794
|
+
|
|
3795
|
+
if (node.getAttribute("data-testid") === "play-button") score += 220;
|
|
3796
|
+
if (label.includes("play")) score += 100;
|
|
3797
|
+
if (background.includes("29, 185, 84") || background.includes("30, 215, 96")) score += 180;
|
|
3798
|
+
if (rect.top <= window.innerHeight * 0.72) score += 90;
|
|
3799
|
+
if (rect.top <= window.innerHeight * 0.56) score += 70;
|
|
3800
|
+
if (rect.left <= window.innerWidth * 0.4) score += 80;
|
|
3801
|
+
if (rect.width >= 44) score += 30;
|
|
3802
|
+
if (rect.height >= 44) score += 30;
|
|
3803
|
+
if (node.closest("[data-testid='entityHeader'], main")) score += 60;
|
|
3804
|
+
score += Math.max(0, 18 - index);
|
|
3805
|
+
|
|
3806
|
+
return score > 0 ? { node, label, score } : null;
|
|
3807
|
+
})
|
|
3808
|
+
.filter(Boolean)
|
|
3809
|
+
.sort((left, right) => right.score - left.score);
|
|
3810
|
+
|
|
3811
|
+
if (!rankedButtons.length) {
|
|
3812
|
+
return null;
|
|
3813
|
+
}
|
|
3814
|
+
|
|
3815
|
+
const winner = rankedButtons[0];
|
|
3816
|
+
return clickElement(
|
|
3817
|
+
winner.node,
|
|
3818
|
+
"safari_dom_spotify_track_page_play",
|
|
3819
|
+
winner.label,
|
|
3820
|
+
location.href || "",
|
|
3821
|
+
winner.score,
|
|
3822
|
+
rankedButtons.length,
|
|
3823
|
+
);
|
|
3824
|
+
}
|
|
3825
|
+
|
|
3826
|
+
const spotifyTrackPagePlay = attemptSpotifyTrackPagePlayClick();
|
|
3827
|
+
if (spotifyTrackPagePlay) {
|
|
3828
|
+
return spotifyTrackPagePlay;
|
|
3829
|
+
}
|
|
3830
|
+
|
|
3831
|
+
function attemptSpotifySearchResultClick() {
|
|
3832
|
+
if (!location.hostname.includes("open.spotify.com")) {
|
|
3833
|
+
return null;
|
|
3834
|
+
}
|
|
3835
|
+
if (!quotedPhrases.length && !tokens.length) {
|
|
3836
|
+
return null;
|
|
3837
|
+
}
|
|
3838
|
+
|
|
3839
|
+
const rows = Array.from(document.querySelectorAll(
|
|
3840
|
+
"[data-testid='tracklist-row'], [role='row']"
|
|
3841
|
+
))
|
|
3842
|
+
.filter((node) => node instanceof HTMLElement)
|
|
3843
|
+
.filter((node) => isVisible(node));
|
|
3844
|
+
|
|
3845
|
+
const rankedRows = rows
|
|
3846
|
+
.map((row, index) => {
|
|
3847
|
+
const titleNode = row.querySelector(
|
|
3848
|
+
"[data-testid='internal-track-link'], a[href*='/track/'], [aria-colindex='2'] a[href*='/track/']"
|
|
3849
|
+
);
|
|
3850
|
+
const navigationTarget = titleNode instanceof HTMLAnchorElement || titleNode instanceof HTMLElement
|
|
3851
|
+
? titleNode
|
|
3852
|
+
: null;
|
|
3853
|
+
const titleText = String((titleNode && titleNode.textContent) || "").trim();
|
|
3854
|
+
const rowText = deriveText(row);
|
|
3855
|
+
const normalizedTitle = normalize(titleText);
|
|
3856
|
+
const normalizedRow = normalize(rowText);
|
|
3857
|
+
let score = 0;
|
|
3858
|
+
|
|
3859
|
+
for (const phrase of quotedPhrases) {
|
|
3860
|
+
if (!phrase) continue;
|
|
3861
|
+
if (normalizedTitle.includes(phrase)) score += 180;
|
|
3862
|
+
else if (normalizedRow.includes(phrase)) score += 110;
|
|
3863
|
+
}
|
|
3864
|
+
|
|
3865
|
+
for (const token of tokens) {
|
|
3866
|
+
if (normalizedTitle.includes(token)) score += 26;
|
|
3867
|
+
else if (normalizedRow.includes(token)) score += 10;
|
|
3868
|
+
}
|
|
3869
|
+
|
|
3870
|
+
const titleMatches = tokens.filter((token) => normalizedTitle.includes(token)).length;
|
|
3871
|
+
if (tokens.length > 1 && titleMatches >= Math.max(2, Math.ceil(tokens.length * 0.5))) score += 90;
|
|
3872
|
+
if (titleMatches === tokens.length && tokens.length > 0) score += 70;
|
|
3873
|
+
if (index < 4) score += Math.max(0, 24 - (index * 5));
|
|
3874
|
+
|
|
3875
|
+
const playCandidate = Array.from(row.querySelectorAll("button, [role='button'], a[href*='/track/']"))
|
|
3876
|
+
.filter((candidate) => candidate instanceof HTMLElement || candidate instanceof HTMLAnchorElement)
|
|
3877
|
+
.filter((candidate) => isVisible(candidate))
|
|
3878
|
+
.map((candidate) => {
|
|
3879
|
+
const label = normalize([
|
|
3880
|
+
deriveText(candidate),
|
|
3881
|
+
candidate.getAttribute("aria-label"),
|
|
3882
|
+
candidate.getAttribute("title"),
|
|
3883
|
+
].filter(Boolean).join(" "));
|
|
3884
|
+
let candidateScore = 0;
|
|
3885
|
+
if (/play|tocar|reproduzir/.test(label)) candidateScore += 40;
|
|
3886
|
+
if (candidate instanceof HTMLAnchorElement && normalize(candidate.href).includes("/track/")) candidateScore += 18;
|
|
3887
|
+
return { candidate, candidateScore };
|
|
3888
|
+
})
|
|
3889
|
+
.sort((left, right) => right.candidateScore - left.candidateScore);
|
|
3890
|
+
|
|
3891
|
+
if (wantsSpotifyTrackPageOpen && !navigationTarget) {
|
|
3892
|
+
return null;
|
|
3893
|
+
}
|
|
3894
|
+
|
|
3895
|
+
return score > 0 ? {
|
|
3896
|
+
row,
|
|
3897
|
+
titleText,
|
|
3898
|
+
href: titleNode instanceof HTMLAnchorElement ? titleNode.href : "",
|
|
3899
|
+
score: score + (playCandidate[0]?.candidateScore || 0),
|
|
3900
|
+
target: wantsSpotifyTrackPageOpen
|
|
3901
|
+
? navigationTarget
|
|
3902
|
+
: (playCandidate[0]?.candidate || navigationTarget || row),
|
|
3903
|
+
} : null;
|
|
3904
|
+
})
|
|
3905
|
+
.filter(Boolean)
|
|
3906
|
+
.sort((left, right) => right.score - left.score);
|
|
3907
|
+
|
|
3908
|
+
if (!rankedRows.length) {
|
|
3909
|
+
return null;
|
|
3910
|
+
}
|
|
3911
|
+
|
|
3912
|
+
const winner = rankedRows[0];
|
|
3913
|
+
const spotifyRow = winner.row instanceof HTMLElement ? winner.row : null;
|
|
3914
|
+
const titleCandidate = spotifyRow?.querySelector(
|
|
3915
|
+
"[data-testid='internal-track-link'], a[href*='/track/'], [aria-colindex='2'] a[href*='/track/']"
|
|
3916
|
+
);
|
|
3917
|
+
const explicitPlayButton = spotifyRow
|
|
3918
|
+
? Array.from(spotifyRow.querySelectorAll("button, [role='button']"))
|
|
3919
|
+
.filter((node) => node instanceof HTMLElement)
|
|
3920
|
+
.filter((node) => isVisible(node))
|
|
3921
|
+
.find((node) => {
|
|
3922
|
+
const label = normalize([
|
|
3923
|
+
deriveText(node),
|
|
3924
|
+
node.getAttribute("aria-label"),
|
|
3925
|
+
node.getAttribute("title"),
|
|
3926
|
+
].filter(Boolean).join(" "));
|
|
3927
|
+
return /\b(play|tocar|reproduzir)\b/.test(label);
|
|
3928
|
+
})
|
|
3929
|
+
: null;
|
|
3930
|
+
|
|
3931
|
+
const clickTarget = wantsSpotifyTrackPageOpen
|
|
3932
|
+
? (titleCandidate || winner.target)
|
|
3933
|
+
: (explicitPlayButton || titleCandidate || winner.target);
|
|
3934
|
+
const clicked = clickElement(
|
|
3935
|
+
clickTarget,
|
|
3936
|
+
wantsSpotifyTrackPageOpen
|
|
3937
|
+
? "safari_dom_spotify_track_link"
|
|
3938
|
+
: (explicitPlayButton ? "safari_dom_spotify_play_button" : "safari_dom_spotify_result"),
|
|
3939
|
+
winner.titleText || deriveText(winner.row),
|
|
3940
|
+
winner.href || "",
|
|
3941
|
+
winner.score,
|
|
3942
|
+
rankedRows.length,
|
|
3943
|
+
);
|
|
3944
|
+
if (!clicked) {
|
|
3945
|
+
return null;
|
|
3946
|
+
}
|
|
3947
|
+
if (spotifyRow instanceof HTMLElement && !explicitPlayButton && !wantsSpotifyTrackPageOpen) {
|
|
3948
|
+
const rect = spotifyRow.getBoundingClientRect();
|
|
3949
|
+
spotifyRow.dispatchEvent(new MouseEvent("dblclick", {
|
|
3950
|
+
bubbles: true,
|
|
3951
|
+
cancelable: true,
|
|
3952
|
+
view: window,
|
|
3953
|
+
clientX: rect.left + (rect.width / 2),
|
|
3954
|
+
clientY: rect.top + (rect.height / 2),
|
|
3955
|
+
}));
|
|
3956
|
+
}
|
|
3957
|
+
return clicked;
|
|
3958
|
+
}
|
|
3959
|
+
|
|
3960
|
+
const spotifyResult = attemptSpotifySearchResultClick();
|
|
3961
|
+
if (spotifyResult) {
|
|
3962
|
+
return spotifyResult;
|
|
3963
|
+
}
|
|
3964
|
+
|
|
3965
|
+
if (location.hostname.includes("open.spotify.com") && (wantsSpotifyTrackPageOpen || wantsSpotifyTrackPagePlay)) {
|
|
3966
|
+
return {
|
|
3967
|
+
clicked: false,
|
|
3968
|
+
reason: wantsSpotifyTrackPageOpen
|
|
3969
|
+
? "Nenhuma faixa visivel do Spotify Web combinou com a busca atual; nao vou clicar em abas, playlists ou albuns por fallback."
|
|
3970
|
+
: "O Spotify Web nao mostrou um botao principal de reproduzir confiavel; nao vou clicar em elementos genericos por fallback.",
|
|
3971
|
+
strategy: "safari_dom_spotify_no_fallback",
|
|
3972
|
+
};
|
|
3973
|
+
}
|
|
3974
|
+
|
|
3137
3975
|
function scoreCandidate(element, rank) {
|
|
3138
3976
|
const text = deriveText(element);
|
|
3139
3977
|
const href = element instanceof HTMLAnchorElement
|
|
@@ -3254,7 +4092,7 @@ tell application "Safari"
|
|
|
3254
4092
|
activate
|
|
3255
4093
|
if (count of windows) = 0 then error "Safari nao possui janelas abertas."
|
|
3256
4094
|
delay 1
|
|
3257
|
-
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');
|
|
4095
|
+
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});})();"
|
|
3258
4096
|
set pageJson to do JavaScript jsCode in current tab of front window
|
|
3259
4097
|
end tell
|
|
3260
4098
|
return pageJson
|