Automatically plays words on SoundsAmerican website with play/stop controls
Size
9.8 KB
Version
1.1.3
Created
Dec 1, 2025
Updated
12 days ago
1// ==UserScript==
2// @name SoundsAmerican Word Player
3// @description Automatically plays words on SoundsAmerican website with play/stop controls
4// @version 1.1.3
5// @match https://*.soundsamerican.net/*
6// @icon https://robomonkey.io/icon.png?adc3438f5fbb5315
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 console.log('🎵 SoundsAmerican Word Player 已启动');
12
13 // 延迟检测容器,确保异步渲染的内容被检测到
14 function waitForContent(callback) {
15 const interval = setInterval(() => {
16 // 检测单词容器
17 const wordContainer = document.querySelector(".sa-most-common-words-container");
18 // 检测音标容器
19 const ipaContainer = document.querySelector("#phtbPhonemesContainer0");
20
21 if (wordContainer || ipaContainer) {
22 clearInterval(interval);
23 callback({ wordContainer, ipaContainer });
24 }
25 }, 500); // 每 0.5 秒检测一次
26 }
27
28 waitForContent(({ wordContainer, ipaContainer }) => {
29 console.log("✅ 内容已检测到,开始注入按钮");
30 console.log("单词容器:", wordContainer ? "存在" : "不存在");
31 console.log("音标容器:", ipaContainer ? "存在" : "不存在");
32
33 // === 创建按钮容器 ===
34 const containerDiv = document.createElement("div");
35 Object.assign(containerDiv.style, {
36 position: "fixed",
37 right: "20px",
38 bottom: "20px",
39 zIndex: "99999",
40 display: "flex",
41 gap: "10px",
42 flexDirection: "column",
43 alignItems: "flex-end"
44 });
45
46 const playBtn = document.createElement("button");
47 const pauseBtn = document.createElement("button");
48
49 const baseStyle = {
50 color: "#fff",
51 border: "none",
52 padding: "10px 16px",
53 borderRadius: "8px",
54 cursor: "pointer",
55 fontSize: "16px",
56 boxShadow: "0 2px 6px rgba(0,0,0,0.3)"
57 };
58
59 Object.assign(playBtn.style, baseStyle, { background: "#007bff" });
60 Object.assign(pauseBtn.style, baseStyle, { background: "#dc3545", display: "none" });
61
62 playBtn.textContent = "▶ 播放";
63 pauseBtn.textContent = "⏸ 暂停播放";
64
65 containerDiv.appendChild(pauseBtn);
66 containerDiv.appendChild(playBtn);
67 document.body.appendChild(containerDiv);
68
69 console.log("✅ 播放按钮已添加到页面");
70
71 // === 播放控制变量 ===
72 let isPlaying = false;
73 let isPaused = false;
74 let audio = null;
75 let currentIndex = 0;
76 let currentRepeat = 0;
77 let playList = [];
78 let playMode = null; // 'words' 或 'ipa'
79
80 // === 播放逻辑 ===
81 function startPlayback() {
82 // 如果是暂停后继续
83 if (isPaused) {
84 isPaused = false;
85 pauseBtn.textContent = "⏸ 暂停播放";
86 if (playMode === 'ipa') {
87 // 音标模式:继续点击
88 continueIpaPlayback();
89 } else {
90 // 单词模式:继续播放音频
91 if (audio) {
92 audio.play();
93 console.log("▶️ 音频继续播放");
94 }
95 }
96 return;
97 }
98
99 // ------- 首次播放:判断播放模式 -------
100 // 优先检测音标按钮
101 const ipaButtons = document.querySelectorAll("#phtbPhonemesContainer0 button.ipa-chart-l__phoneme");
102 const wordItems = document.querySelectorAll(".sa-tooltip-indicator[data-audio-id]");
103
104 if (ipaButtons.length > 0) {
105 // 音标模式
106 playMode = 'ipa';
107 startIpaPlayback(ipaButtons);
108 } else if (wordItems.length > 0) {
109 // 单词模式
110 playMode = 'words';
111 startWordPlayback(wordItems);
112 } else {
113 alert("⚠️ 未找到可播放的内容,请确认页面已加载完毕。");
114 console.error("❌ 未找到音标或单词元素");
115 return;
116 }
117 }
118
119 // === 音标播放模式 ===
120 function startIpaPlayback(ipaButtons) {
121 console.log(`🎵 找到 ${ipaButtons.length} 个音标,开始播放(每个点击3次)`);
122 playBtn.disabled = true;
123 playBtn.textContent = "🎧 播放中…";
124 pauseBtn.style.display = "block";
125 isPlaying = true;
126
127 playList = Array.from(ipaButtons).filter(btn => btn.textContent.trim() !== ''); // 过滤空按钮
128 currentIndex = 0;
129 currentRepeat = 0;
130
131 playNextIpa();
132 }
133
134 function playNextIpa() {
135 if (isPaused) {
136 console.log("⏸ 播放已暂停");
137 return;
138 }
139
140 if (currentIndex >= playList.length) {
141 playBtn.textContent = "✅ 播放完成";
142 playBtn.disabled = false;
143 pauseBtn.style.display = "none";
144 isPlaying = false;
145 console.log("✅ 所有音标播放完成");
146 return;
147 }
148
149 const button = playList[currentIndex];
150 const ipaText = button.textContent.trim();
151 console.log(`▶️ 点击第 ${currentIndex + 1}/${playList.length} 个音标:${ipaText}(第 ${currentRepeat + 1} 次)`);
152
153 // 点击按钮
154 button.click();
155
156 currentRepeat++;
157 if (currentRepeat < 3) {
158 // 继续点击同一个音标
159 setTimeout(playNextIpa, 1000); // 每次点击间隔1秒
160 } else {
161 // 下一个音标
162 currentRepeat = 0;
163 currentIndex++;
164 setTimeout(playNextIpa, 1500); // 音标之间停顿1.5秒
165 }
166 }
167
168 function continueIpaPlayback() {
169 console.log("▶️ 继续播放音标");
170 playNextIpa();
171 }
172
173 // === 单词播放模式 ===
174 function startWordPlayback(wordItems) {
175 console.log(`🎵 找到 ${wordItems.length} 个单词,开始播放`);
176 playBtn.disabled = true;
177 playBtn.textContent = "🎧 播放中…";
178 pauseBtn.style.display = "block";
179 isPlaying = true;
180
181 playList = Array.from(wordItems).map(el => ({
182 id: el.getAttribute("data-audio-id"),
183 el
184 }));
185
186 currentIndex = 0;
187 currentRepeat = 0;
188 audio = new Audio();
189
190 const triggerClick = el => {
191 const evt = new MouseEvent("click", { bubbles: true, cancelable: true, view: window });
192 el.dispatchEvent(evt);
193 };
194
195 const hideTooltips = () => {
196 document.querySelectorAll(".sa-tooltip.active").forEach(t => t.classList.remove("active"));
197 };
198
199 const playNext = () => {
200 if (isPaused) return;
201
202 if (currentIndex >= playList.length) {
203 hideTooltips();
204 playBtn.textContent = "✅ 播放完成";
205 playBtn.disabled = false;
206 pauseBtn.style.display = "none";
207 isPlaying = false;
208 console.log("✅ 所有单词播放完成");
209 return;
210 }
211
212 const { id, el } = playList[currentIndex];
213 console.log(`▶️ 播放第 ${currentIndex + 1}/${playList.length} 个单词:${id}(第 ${currentRepeat + 1} 遍)`);
214
215 triggerClick(el);
216 audio.src = `/audio/words/${id}.mp3`;
217 audio.play().catch(e => {
218 console.warn("⚠️ 播放被阻止,请点击页面任意位置以启用音频播放", e);
219 document.body.addEventListener("click", () => audio.play(), { once: true });
220 });
221 };
222
223 audio.addEventListener("ended", () => {
224 if (isPaused) return;
225
226 currentRepeat++;
227 if (currentRepeat < 2) {
228 playNext();
229 } else {
230 currentRepeat = 0;
231 currentIndex++;
232 setTimeout(playNext, 800);
233 }
234 });
235
236 playNext();
237 }
238
239 // === 暂停 / 继续播放 ===
240 function togglePause() {
241 if (!isPlaying && !isPaused) return;
242
243 if (!isPaused) {
244 // --- 暂停 ---
245 isPaused = true;
246 pauseBtn.textContent = "▶ 继续播放";
247 console.log("⏸ 播放已暂停");
248 if (playMode === 'words' && audio) {
249 audio.pause();
250 }
251 } else {
252 // --- 继续 ---
253 isPaused = false;
254 pauseBtn.textContent = "⏸ 暂停播放";
255 console.log("▶️ 播放继续");
256 if (playMode === 'words' && audio) {
257 audio.play();
258 } else if (playMode === 'ipa') {
259 continueIpaPlayback();
260 }
261 }
262 }
263
264 // === 事件绑定 ===
265 playBtn.addEventListener("click", startPlayback);
266 pauseBtn.addEventListener("click", togglePause);
267
268 // === 按钮 hover 效果 ===
269 playBtn.onmouseenter = () => (playBtn.style.background = "#0056b3");
270 playBtn.onmouseleave = () => (playBtn.style.background = "#007bff");
271 pauseBtn.onmouseenter = () => (pauseBtn.style.background = "#b02a37");
272 pauseBtn.onmouseleave = () => (pauseBtn.style.background = "#dc3545");
273 });
274})();