test.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. /*
  2. * qqmusic_play_hongshanguo.js (稳定版 v3)
  3. * 通过 AutoX + QQ音乐 播放《红山果》
  4. *
  5. * 设计目标:
  6. * 1. 稳定可靠 —— 关键步骤带重试 / 轮询,不依赖固定 sleep。
  7. * 2. 不受网络不稳定影响 —— 搜索结果、播放缓冲采用“长轮询 + 重试 + 心跳”,
  8. * 轮询期间持续打印心跳,避免工具因静默超时断开。
  9. * 3. 未登录直接用游客态播放 —— 主动跳过登录/协议页,绝不卡在登录墙。
  10. * 4. 结果落盘 —— 末尾把结论写入 /sdcard/qqmusic_result.txt,便于离线核验。
  11. *
  12. * 关键实现说明:
  13. * - 搜索结果列表项:文字在“不可点击的 TextView”上,真正可点击的是其“父行”,
  14. * 故点击结果时必须找“含目标文字节点的最近可点击祖先”,而非要求自身可点击。
  15. * - 搜索框容器 clickable 可能为 false,点击其中心即可。
  16. */
  17. var PKG = "com.tencent.qqmusic";
  18. var TARGET = "红山果";
  19. var RESULT = "UNKNOWN";
  20. function log(s) { console.log(s); }
  21. function saveAndEnd() {
  22. try { files.write("/sdcard/qqmusic_result.txt", RESULT + " | " + new Date().toLocaleString()); } catch (e) { }
  23. log("RESULT=" + RESULT);
  24. throw new Error("END_SCRIPT");
  25. }
  26. function waitFor(cond, timeoutMs, intervalMs) {
  27. intervalMs = intervalMs || 500;
  28. var elapsed = 0, hb = 0;
  29. while (elapsed < timeoutMs) {
  30. if (cond()) return true;
  31. sleep(intervalMs); elapsed += intervalMs; hb += intervalMs;
  32. if (hb >= 2000) { log("..."); hb = 0; }
  33. }
  34. return cond();
  35. }
  36. function findNode(matcher) {
  37. try {
  38. var root = auto.root; if (!root) return null; var stack = [root];
  39. while (stack.length) {
  40. var node = stack.pop(); if (!node) continue;
  41. if (matcher(node)) return node;
  42. var n = node.childCount(); for (var i = 0; i < n; i++) stack.push(node.child(i));
  43. }
  44. } catch (e) { }
  45. return null;
  46. }
  47. function findAll(matcher) {
  48. var out = [];
  49. try {
  50. var root = auto.root; if (!root) return out; var stack = [root];
  51. while (stack.length) {
  52. var node = stack.pop(); if (!node) continue;
  53. if (matcher(node)) out.push(node);
  54. var n = node.childCount(); for (var i = 0; i < n; i++) stack.push(node.child(i));
  55. }
  56. } catch (e) { }
  57. return out;
  58. }
  59. // 文字包含 kw 的节点(排除输入框)
  60. function hasText(kw) {
  61. return findNode(function (n) {
  62. if (n.className() && n.className().indexOf("EditText") >= 0) return false;
  63. var t = n.text() || ""; return t.indexOf(kw) >= 0;
  64. }) != null;
  65. }
  66. // 点击“含目标文字节点的最近可点击祖先”(用于结果行)
  67. function tapResultRow(prefList) {
  68. var matches = findAll(function (n) {
  69. if (n.className() && n.className().indexOf("EditText") >= 0) return false;
  70. var t = n.text() || ""; return t.indexOf(TARGET) >= 0;
  71. });
  72. if (!matches.length) return false;
  73. // 优先匹配偏好关键词(如原唱“安与骑兵”、或“单曲”)
  74. var chosen = null;
  75. for (var p = 0; p < (prefList || []).length; p++) {
  76. for (var i = 0; i < matches.length; i++) {
  77. if ((matches[i].text() || "").indexOf(prefList[p]) >= 0) { chosen = matches[i]; break; }
  78. }
  79. if (chosen) break;
  80. }
  81. if (!chosen) chosen = matches[0];
  82. var node = chosen;
  83. while (node) {
  84. if (node.clickable() && (!node.className() || node.className().indexOf("EditText") < 0)) {
  85. click(node.bounds().centerX(), node.bounds().centerY()); return true;
  86. }
  87. node = node.parent();
  88. }
  89. return false;
  90. }
  91. function tapIdOnce(idStr) { var n = id(idStr).findOnce(); if (n) { click(n.bounds().centerX(), n.bounds().centerY()); return true; } return false; }
  92. function tapTextOnce(txt) { var n = text(txt).findOnce(); if (n) { click(n.bounds().centerX(), n.bounds().centerY()); return true; } return false; }
  93. function tapDescOnce(d) { var n = desc(d).findOnce(); if (n) { click(n.bounds().centerX(), n.bounds().centerY()); return true; } return false; }
  94. function isPlaying() {
  95. return findNode(function (node) {
  96. var t = node.text() || ""; var d = node.desc() || "";
  97. return (t.indexOf("暂停") >= 0 || d.indexOf("暂停") >= 0);
  98. }) != null;
  99. }
  100. function findPlayButton() {
  101. return findNode(function (node) {
  102. if (node.className() && node.className().indexOf("EditText") >= 0) return false;
  103. var t = node.text() || ""; var d = node.desc() || "";
  104. return (t.indexOf("播放") >= 0 || d.indexOf("播放") >= 0) && node.clickable();
  105. });
  106. }
  107. function onMainPage() {
  108. if (currentPackage() !== PKG) return false;
  109. var s = findNode(function (n) { var d = n.desc() || ""; return d.indexOf("搜索") >= 0; });
  110. if (s) return true;
  111. var h = findNode(function (n) { var t = n.text() || ""; return t.indexOf("首页") >= 0; });
  112. return h != null;
  113. }
  114. /* ---------- 1. 启动 ---------- */
  115. log("=== 1. 启动 QQ音乐 ===");
  116. try { home(); } catch (e) { } // 先回桌面,清掉残留的搜索/键盘焦点
  117. sleep(2000);
  118. var launched = false;
  119. for (var i = 0; i < 3; i++) {
  120. try { app.launchPackage(PKG); } catch (e) { try { app.launch(PKG); } catch (e2) { } }
  121. if (waitFor(function () { return currentPackage() === PKG; }, 10000)) { launched = true; break; }
  122. sleep(1000);
  123. }
  124. if (!launched) { RESULT = "ERR_LAUNCH"; log("ERR: 无法启动"); saveAndEnd(); }
  125. /* ---------- 2. 等待主页面(顺带跳过登录页)---------- */
  126. log("=== 2. 等待主页面 / 跳过登录(游客态)===");
  127. var mainOK = false;
  128. for (var g = 0; g < 10; g++) {
  129. if (onMainPage()) { mainOK = true; break; }
  130. log("尚未进入主页面,尝试跳过登录… (" + g + ")");
  131. tapIdOnce("com.tencent.qqmusic:id/fbi"); sleep(1000); if (onMainPage()) { mainOK = true; break; }
  132. tapTextOnce("取消"); sleep(1000); if (onMainPage()) { mainOK = true; break; }
  133. tapDescOnce("返回"); sleep(1000); if (onMainPage()) { mainOK = true; break; }
  134. tapTextOnce("跳过"); sleep(1000);
  135. }
  136. if (!mainOK) { RESULT = "ERR_NO_MAIN"; log("ERR: 始终未进入主页面"); saveAndEnd(); }
  137. log("已进入主页面");
  138. /* ---------- 3. 打开搜索框 ---------- */
  139. log("=== 3. 打开搜索框 ===");
  140. var searchBar = null;
  141. for (var k = 0; k < 10; k++) {
  142. searchBar = findNode(function (n) { var d = n.desc() || ""; return d.indexOf("搜索") >= 0; });
  143. if (searchBar) break; sleep(600);
  144. }
  145. if (!searchBar) { log("WARN: 坐标兜底点击搜索栏"); click(495, 96); sleep(1500); }
  146. else { click(searchBar.bounds().centerX(), searchBar.bounds().centerY()); sleep(1500); }
  147. /* ---------- 4. 输入 ---------- */
  148. log("=== 4. 输入 " + TARGET + " ===");
  149. var editBox = className("android.widget.EditText").findOne(4000);
  150. if (!editBox) { RESULT = "ERR_NO_EDIT"; log("ERR: 搜索输入框未出现"); saveAndEnd(); }
  151. editBox.setText(""); sleep(1000); editBox.setText(TARGET); sleep(2000)
  152. var c = className("android.widget.EditText").findOne(2000);
  153. log("搜索框内容: " + (c ? c.text() : "null"));
  154. sleep(1500);
  155. /* ---------- 5. 等待结果(网络容错)---------- */
  156. log("=== 5. 等待搜索结果(≤18s) ===");
  157. var ok = waitFor(function () { return hasText(TARGET); }, 18000, 800);
  158. if (!ok) { RESULT = "ERR_NO_RESULT"; log("ERR: 搜索结果未出现"); saveAndEnd(); }
  159. log("结果已出现");
  160. /* ---------- 6. 点击《红山果》结果(优先原唱 安与骑兵)---------- */
  161. log("=== 6. 点击《" + TARGET + "》歌曲 ===");
  162. if (!tapResultRow(["安与骑兵", "单曲", "儿歌", "童声"])) { RESULT = "ERR_NO_TAP"; log("ERR: 未点到结果"); saveAndEnd(); }
  163. sleep(3000);
  164. // 若仍在搜索输入态(仅补全了文字未跳转),提交搜索(回车)后再点一次
  165. if (!isPlaying() && className("android.widget.EditText").findOne(1500)) {
  166. log("尝试提交搜索(回车)");
  167. try { shell("input keyevent 66"); } catch (e) { log("shellErr:" + e); }
  168. sleep(2500);
  169. waitFor(function () { return hasText(TARGET); }, 12000, 800);
  170. tapResultRow(["安与骑兵", "单曲", "儿歌", "童声"]);
  171. sleep(3000);
  172. }
  173. /* ---------- 7. 播放(游客态 + 重试)---------- */
  174. log("=== 7. 播放 ===");
  175. if (isPlaying()) { RESULT = "OK_PLAYING"; log("已在播放"); saveAndEnd(); }
  176. var playOk = false;
  177. for (var p = 0; p < 6 && !playOk; p++) {
  178. log("播放尝试 " + (p + 1));
  179. var pb = findPlayButton(); if (pb) { click(pb.bounds().centerX(), pb.bounds().centerY()); }
  180. playOk = waitFor(isPlaying, 6000, 500);
  181. if (playOk) break;
  182. tapResultRow(["安与骑兵", "单曲", "儿歌", "童声"]); sleep(1000);
  183. let res1 = id("com.tencent.qqmusic:id/k0i").findOne(1000)
  184. console.log(res1)
  185. if (res1) {
  186. let hongShanGuo = res1.findOne(text("红山果"))
  187. let anQiBin = res1.findOne(text("安与骑兵·安与骑兵"))
  188. if (hongShanGuo && anQiBin) {
  189. console.log("找到歌曲")
  190. click(hongShanGuo.bounds().centerX(), hongShanGuo.bounds().centerY())
  191. sleep(2000)
  192. break
  193. }
  194. }
  195. }
  196. if (playOk) { RESULT = "OK_PLAYING"; log("SUCCESS: 《" + TARGET + "》已开始播放(游客态)"); }
  197. else {
  198. log("WARN: 未检测到播放态,尝试兜底电台");
  199. var fb = tapDescOnce("播放猜你喜欢") || tapDescOnce("播放");
  200. if (fb) playOk = waitFor(isPlaying, 10000, 500);
  201. if (playOk) { RESULT = "OK_PLAYING_RADIO"; log("SUCCESS(兜底电台): 已进入播放"); }
  202. else { RESULT = "FAIL_NEED_LOGIN"; log("FAIL: 游客态无法播放该指定歌曲,需登录后才能播放"); }
  203. }
  204. saveAndEnd();