/* * qqmusic_play_hongshanguo.js (稳定版 v3) * 通过 AutoX + QQ音乐 播放《红山果》 * * 设计目标: * 1. 稳定可靠 —— 关键步骤带重试 / 轮询,不依赖固定 sleep。 * 2. 不受网络不稳定影响 —— 搜索结果、播放缓冲采用“长轮询 + 重试 + 心跳”, * 轮询期间持续打印心跳,避免工具因静默超时断开。 * 3. 未登录直接用游客态播放 —— 主动跳过登录/协议页,绝不卡在登录墙。 * 4. 结果落盘 —— 末尾把结论写入 /sdcard/qqmusic_result.txt,便于离线核验。 * * 关键实现说明: * - 搜索结果列表项:文字在“不可点击的 TextView”上,真正可点击的是其“父行”, * 故点击结果时必须找“含目标文字节点的最近可点击祖先”,而非要求自身可点击。 * - 搜索框容器 clickable 可能为 false,点击其中心即可。 */ var PKG = "com.tencent.qqmusic"; var TARGET = "红山果"; var RESULT = "UNKNOWN"; function log(s) { console.log(s); } function saveAndEnd() { try { files.write("/sdcard/qqmusic_result.txt", RESULT + " | " + new Date().toLocaleString()); } catch (e) { } log("RESULT=" + RESULT); throw new Error("END_SCRIPT"); } function waitFor(cond, timeoutMs, intervalMs) { intervalMs = intervalMs || 500; var elapsed = 0, hb = 0; while (elapsed < timeoutMs) { if (cond()) return true; sleep(intervalMs); elapsed += intervalMs; hb += intervalMs; if (hb >= 2000) { log("..."); hb = 0; } } return cond(); } function findNode(matcher) { try { var root = auto.root; if (!root) return null; var stack = [root]; while (stack.length) { var node = stack.pop(); if (!node) continue; if (matcher(node)) return node; var n = node.childCount(); for (var i = 0; i < n; i++) stack.push(node.child(i)); } } catch (e) { } return null; } function findAll(matcher) { var out = []; try { var root = auto.root; if (!root) return out; var stack = [root]; while (stack.length) { var node = stack.pop(); if (!node) continue; if (matcher(node)) out.push(node); var n = node.childCount(); for (var i = 0; i < n; i++) stack.push(node.child(i)); } } catch (e) { } return out; } // 文字包含 kw 的节点(排除输入框) function hasText(kw) { return findNode(function (n) { if (n.className() && n.className().indexOf("EditText") >= 0) return false; var t = n.text() || ""; return t.indexOf(kw) >= 0; }) != null; } // 点击“含目标文字节点的最近可点击祖先”(用于结果行) function tapResultRow(prefList) { var matches = findAll(function (n) { if (n.className() && n.className().indexOf("EditText") >= 0) return false; var t = n.text() || ""; return t.indexOf(TARGET) >= 0; }); if (!matches.length) return false; // 优先匹配偏好关键词(如原唱“安与骑兵”、或“单曲”) var chosen = null; for (var p = 0; p < (prefList || []).length; p++) { for (var i = 0; i < matches.length; i++) { if ((matches[i].text() || "").indexOf(prefList[p]) >= 0) { chosen = matches[i]; break; } } if (chosen) break; } if (!chosen) chosen = matches[0]; var node = chosen; while (node) { if (node.clickable() && (!node.className() || node.className().indexOf("EditText") < 0)) { click(node.bounds().centerX(), node.bounds().centerY()); return true; } node = node.parent(); } return false; } function tapIdOnce(idStr) { var n = id(idStr).findOnce(); if (n) { click(n.bounds().centerX(), n.bounds().centerY()); return true; } return false; } function tapTextOnce(txt) { var n = text(txt).findOnce(); if (n) { click(n.bounds().centerX(), n.bounds().centerY()); return true; } return false; } function tapDescOnce(d) { var n = desc(d).findOnce(); if (n) { click(n.bounds().centerX(), n.bounds().centerY()); return true; } return false; } function isPlaying() { return findNode(function (node) { var t = node.text() || ""; var d = node.desc() || ""; return (t.indexOf("暂停") >= 0 || d.indexOf("暂停") >= 0); }) != null; } function findPlayButton() { return findNode(function (node) { if (node.className() && node.className().indexOf("EditText") >= 0) return false; var t = node.text() || ""; var d = node.desc() || ""; return (t.indexOf("播放") >= 0 || d.indexOf("播放") >= 0) && node.clickable(); }); } function onMainPage() { if (currentPackage() !== PKG) return false; var s = findNode(function (n) { var d = n.desc() || ""; return d.indexOf("搜索") >= 0; }); if (s) return true; var h = findNode(function (n) { var t = n.text() || ""; return t.indexOf("首页") >= 0; }); return h != null; } /* ---------- 1. 启动 ---------- */ log("=== 1. 启动 QQ音乐 ==="); try { home(); } catch (e) { } // 先回桌面,清掉残留的搜索/键盘焦点 sleep(2000); var launched = false; for (var i = 0; i < 3; i++) { try { app.launchPackage(PKG); } catch (e) { try { app.launch(PKG); } catch (e2) { } } if (waitFor(function () { return currentPackage() === PKG; }, 10000)) { launched = true; break; } sleep(1000); } if (!launched) { RESULT = "ERR_LAUNCH"; log("ERR: 无法启动"); saveAndEnd(); } /* ---------- 2. 等待主页面(顺带跳过登录页)---------- */ log("=== 2. 等待主页面 / 跳过登录(游客态)==="); var mainOK = false; for (var g = 0; g < 10; g++) { if (onMainPage()) { mainOK = true; break; } log("尚未进入主页面,尝试跳过登录… (" + g + ")"); tapIdOnce("com.tencent.qqmusic:id/fbi"); sleep(1000); if (onMainPage()) { mainOK = true; break; } tapTextOnce("取消"); sleep(1000); if (onMainPage()) { mainOK = true; break; } tapDescOnce("返回"); sleep(1000); if (onMainPage()) { mainOK = true; break; } tapTextOnce("跳过"); sleep(1000); } if (!mainOK) { RESULT = "ERR_NO_MAIN"; log("ERR: 始终未进入主页面"); saveAndEnd(); } log("已进入主页面"); /* ---------- 3. 打开搜索框 ---------- */ log("=== 3. 打开搜索框 ==="); var searchBar = null; for (var k = 0; k < 10; k++) { searchBar = findNode(function (n) { var d = n.desc() || ""; return d.indexOf("搜索") >= 0; }); if (searchBar) break; sleep(600); } if (!searchBar) { log("WARN: 坐标兜底点击搜索栏"); click(495, 96); sleep(1500); } else { click(searchBar.bounds().centerX(), searchBar.bounds().centerY()); sleep(1500); } /* ---------- 4. 输入 ---------- */ log("=== 4. 输入 " + TARGET + " ==="); var editBox = className("android.widget.EditText").findOne(4000); if (!editBox) { RESULT = "ERR_NO_EDIT"; log("ERR: 搜索输入框未出现"); saveAndEnd(); } editBox.setText(""); sleep(1000); editBox.setText(TARGET); sleep(2000) var c = className("android.widget.EditText").findOne(2000); log("搜索框内容: " + (c ? c.text() : "null")); sleep(1500); /* ---------- 5. 等待结果(网络容错)---------- */ log("=== 5. 等待搜索结果(≤18s) ==="); var ok = waitFor(function () { return hasText(TARGET); }, 18000, 800); if (!ok) { RESULT = "ERR_NO_RESULT"; log("ERR: 搜索结果未出现"); saveAndEnd(); } log("结果已出现"); /* ---------- 6. 点击《红山果》结果(优先原唱 安与骑兵)---------- */ log("=== 6. 点击《" + TARGET + "》歌曲 ==="); if (!tapResultRow(["安与骑兵", "单曲", "儿歌", "童声"])) { RESULT = "ERR_NO_TAP"; log("ERR: 未点到结果"); saveAndEnd(); } sleep(3000); // 若仍在搜索输入态(仅补全了文字未跳转),提交搜索(回车)后再点一次 if (!isPlaying() && className("android.widget.EditText").findOne(1500)) { log("尝试提交搜索(回车)"); try { shell("input keyevent 66"); } catch (e) { log("shellErr:" + e); } sleep(2500); waitFor(function () { return hasText(TARGET); }, 12000, 800); tapResultRow(["安与骑兵", "单曲", "儿歌", "童声"]); sleep(3000); } /* ---------- 7. 播放(游客态 + 重试)---------- */ log("=== 7. 播放 ==="); if (isPlaying()) { RESULT = "OK_PLAYING"; log("已在播放"); saveAndEnd(); } var playOk = false; for (var p = 0; p < 6 && !playOk; p++) { log("播放尝试 " + (p + 1)); var pb = findPlayButton(); if (pb) { click(pb.bounds().centerX(), pb.bounds().centerY()); } playOk = waitFor(isPlaying, 6000, 500); if (playOk) break; tapResultRow(["安与骑兵", "单曲", "儿歌", "童声"]); sleep(1000); let res1 = id("com.tencent.qqmusic:id/k0i").findOne(1000) console.log(res1) if (res1) { let hongShanGuo = res1.findOne(text("红山果")) let anQiBin = res1.findOne(text("安与骑兵·安与骑兵")) if (hongShanGuo && anQiBin) { console.log("找到歌曲") click(hongShanGuo.bounds().centerX(), hongShanGuo.bounds().centerY()) sleep(2000) break } } } if (playOk) { RESULT = "OK_PLAYING"; log("SUCCESS: 《" + TARGET + "》已开始播放(游客态)"); } else { log("WARN: 未检测到播放态,尝试兜底电台"); var fb = tapDescOnce("播放猜你喜欢") || tapDescOnce("播放"); if (fb) playOk = waitFor(isPlaying, 10000, 500); if (playOk) { RESULT = "OK_PLAYING_RADIO"; log("SUCCESS(兜底电台): 已进入播放"); } else { RESULT = "FAIL_NEED_LOGIN"; log("FAIL: 游客态无法播放该指定歌曲,需登录后才能播放"); } } saveAndEnd();