Apple Music 歌词增强

为网页版 Apple Music 提供翻译歌词,数据来源为网易云音乐。

  1. // ==UserScript==
  2. // @name Apple Music 歌词增强
  3. // @namespace https://github.com/akashiwest/AML-Enhancer
  4. // @version 1.100
  5. // @description 为网页版 Apple Music 提供翻译歌词,数据来源为网易云音乐。
  6. // @author Akashi
  7. // @license GNU GPL 3.0
  8. // @match https://*.music.apple.com/*
  9. // @grant GM_xmlhttpRequest
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. // 定义扩展和缩小时容器的高度
  16. const expandedHeight = '240px';
  17. const minimizedHeight = '130px';
  18.  
  19. // 全局变量保存主歌词与翻译歌词数据
  20. let currentLyrics = [];
  21. let currentTLyrics = [];
  22.  
  23. // 创建固定容器(含内部内容容器)用于显示歌词
  24. function createLyricsDisplay() {
  25. const lyricsDiv = document.createElement('div');
  26. lyricsDiv.id = 'lyrics-display';
  27. // 默认缩小
  28. lyricsDiv.dataset.isMinimized = 'true';
  29. Object.assign(lyricsDiv.style, {
  30. position: 'fixed',
  31. right: '20px',
  32. top: '60px',
  33. width: '850px',
  34. height: minimizedHeight,
  35. overflow: 'hidden',
  36. borderRadius: '20px',
  37. backdropFilter: 'saturate(200%) blur(25px)',
  38. background: 'rgba(250,250,250,0.72)',
  39. zIndex: '9999',
  40. padding: '20px 30px',
  41. fontSize: '28px',
  42. color: '#565656',
  43. textAlign: 'center',
  44. boxShadow: '0 5px 30px rgba(0, 0, 0, 0.4)',
  45. fontWeight: 'bold',
  46. msOverflowStyle: 'none',
  47. scrollbarWidth: 'none',
  48. // 高度切换的动画效果,仅对 height 生效
  49. transition: 'height 0.3s ease'
  50. });
  51.  
  52. // 内部容器,滚动时带平滑动画
  53. const lyricsContent = document.createElement('div');
  54. lyricsContent.id = 'lyrics-content';
  55. lyricsContent.style.transition = 'transform 0.3s ease-out';
  56. lyricsContent.style.transform = 'translateY(0)';
  57. lyricsDiv.appendChild(lyricsContent);
  58.  
  59. const defaultGroup = document.createElement('div');
  60. defaultGroup.className = 'lyric-group';
  61. defaultGroup.style.height = '70px';
  62. defaultGroup.style.marginBottom = '10px';
  63.  
  64. const defaultText = document.createElement('div');
  65. defaultText.className = 'main-lyric';
  66. defaultText.innerText = 'Apple Music 歌词翻译 V1.1';
  67. defaultText.style.fontSize = '32px';
  68. defaultText.style.color = '#252525';
  69. defaultText.style.fontWeight = 'bold';
  70. defaultText.style.marginTop = '25px';
  71.  
  72. defaultGroup.appendChild(defaultText);
  73. lyricsContent.appendChild(defaultGroup);
  74. lyricsDiv.appendChild(lyricsContent);
  75.  
  76. // 切换按钮
  77. const toggleButton = document.createElement('button');
  78. const infoButton = document.createElement('button');
  79. toggleButton.id = 'toggle-size-button';
  80. infoButton.id = 'info-button';
  81.  
  82. toggleButton.innerText = '放大';
  83. Object.assign(toggleButton.style, {
  84. position: 'absolute',
  85. bottom: '10px',
  86. right: '10px',
  87. padding: '5px 10px',
  88. fontSize: '14px',
  89. border: 'none',
  90. borderRadius: '15px',
  91. background: '#ddd',
  92. cursor: 'pointer',
  93. zIndex: '10000',
  94. opacity: '0.8'
  95. });
  96. infoButton.innerText = 'O';
  97. Object.assign(infoButton.style, {
  98. position: 'absolute',
  99. bottom: '10px',
  100. right: '65px',
  101. padding: '5px 8px',
  102. fontSize: '14px',
  103. border: 'none',
  104. borderRadius: '15px',
  105. background: '#ddd',
  106. cursor: 'pointer',
  107. zIndex: '10000',
  108. opacity: '0.3'
  109. });
  110. toggleButton.addEventListener('click', function(e) {
  111. // 阻止事件向拖拽处理传播
  112. e.stopPropagation();
  113. if (lyricsDiv.dataset.isMinimized === 'true') {
  114. // 扩展
  115. lyricsDiv.style.height = expandedHeight;
  116. lyricsDiv.dataset.isMinimized = 'false';
  117. toggleButton.innerText = '缩小';
  118. } else {
  119. // 缩小
  120. lyricsDiv.style.height = minimizedHeight;
  121. lyricsDiv.dataset.isMinimized = 'true';
  122. toggleButton.innerText = '放大';
  123. }
  124. });
  125. lyricsDiv.appendChild(toggleButton);
  126. lyricsDiv.appendChild(infoButton);
  127. infoButton.addEventListener('click', function(e) {
  128. e.stopPropagation();
  129. window.open('https://github.com/akashiwest/AML-Enhancer', '_blank');
  130. });
  131.  
  132. // 拖拽功能
  133. lyricsDiv.onmousedown = dragMouseDown;
  134. let pos3 = 0, pos4 = 0;
  135. function dragMouseDown(e) {
  136. // 点击按钮不触发
  137. if (e.target === toggleButton) return;
  138. e.preventDefault();
  139. pos3 = e.clientX;
  140. pos4 = e.clientY;
  141. document.onmouseup = closeDragElement;
  142. document.onmousemove = elementDrag;
  143. }
  144. function elementDrag(e) {
  145. e.preventDefault();
  146. const pos1 = pos3 - e.clientX;
  147. const pos2 = pos4 - e.clientY;
  148. pos3 = e.clientX;
  149. pos4 = e.clientY;
  150. lyricsDiv.style.top = `${lyricsDiv.offsetTop - pos2}px`;
  151. lyricsDiv.style.left = `${lyricsDiv.offsetLeft - pos1}px`;
  152. }
  153. function closeDragElement() {
  154. document.onmouseup = null;
  155. document.onmousemove = null;
  156. }
  157. document.body.appendChild(lyricsDiv);
  158. return lyricsDiv;
  159. }
  160.  
  161. // 根据播放器 title 获取歌曲ID,并调用歌词接口
  162. function getSongId() {
  163. const audioPlayer = document.getElementById('apple-music-player');
  164. if (!audioPlayer) {
  165. console.log('当前页面未找到音频播放器');
  166. return;
  167. }
  168. let title = audioPlayer.title;
  169. // 取标题中第一个“-”前面的部分(可根据实际情况调整)
  170. const secondDashIndex = title.indexOf('-', title.indexOf('-') + 1);
  171. if (secondDashIndex !== -1) {
  172. title = title.substring(0, secondDashIndex).trim();
  173. }
  174. showMessage(title);
  175. const apiUrl = `https://music.163.com/api/search/pc?s=${encodeURIComponent(title)}&offset=0&limit=1&type=1`;
  176. GM_xmlhttpRequest({
  177. method: "GET",
  178. url: apiUrl,
  179. responseType: "json",
  180. onload: function(response) {
  181. if (response.status === 200) {
  182. const data = response.response;
  183. if (data.result && data.result.songs && data.result.songs.length > 0) {
  184. const firstSongId = data.result.songs[0].id;
  185. getLyrics(firstSongId);
  186. console.log(apiUrl, 'ID - ' + firstSongId);
  187. } else {
  188. showMessage("未找到歌曲");
  189. }
  190. } else {
  191. showMessage("请求失败");
  192. }
  193. },
  194. onerror: function() {
  195. console.error("未知错误");
  196. }
  197. });
  198. }
  199.  
  200. // 提示信息
  201. function showMessage(msg) {
  202. const lyricsContent = document.getElementById('lyrics-content');
  203. if (lyricsContent) {
  204. const messageGroup = document.createElement('div');
  205. messageGroup.className = 'lyric-group';
  206. messageGroup.style.height = '70px';
  207. messageGroup.style.marginBottom = '10px';
  208.  
  209. const messageText = document.createElement('div');
  210. messageText.className = 'main-lyric';
  211. messageText.innerText = msg;
  212. messageText.style.fontSize = '32px';
  213. messageText.style.color = '#252525';
  214. messageText.style.fontWeight = 'bold';
  215. messageText.style.filter = 'blur(0) !important';
  216.  
  217. // 添加容器样式以确保垂直居中
  218. messageGroup.style.display = 'flex';
  219. messageGroup.style.alignItems = 'center';
  220. messageGroup.style.justifyContent = 'center';
  221. messageGroup.style.height = '100%';
  222. messageGroup.style.marginTop = '25px';
  223.  
  224. messageGroup.appendChild(messageText);
  225. lyricsContent.innerHTML = '';
  226. lyricsContent.appendChild(messageGroup);
  227. }
  228. }
  229.  
  230. // 获取歌词(同时获取主歌词和翻译歌词)并解析后渲染
  231. function getLyrics(songId) {
  232. const apiUrl = `https://music.163.com/api/song/lyric?lv=1&kv=1&tv=-1&id=${songId}`;
  233. showMessage('歌词正在加载中 ...');
  234.  
  235. GM_xmlhttpRequest({
  236. method: "GET",
  237. url: apiUrl,
  238. responseType: "json",
  239. onload: function(response) {
  240. if (response.status === 200) {
  241. const data = response.response;
  242. if (!data || (!data.lrc && !data.tlyric)) {
  243. showMessage('未找到匹配歌词');
  244. currentLyrics = [];
  245. currentTLyrics = [];
  246. return;
  247. }
  248.  
  249. const lyricsLines = data.lrc ? data.lrc.lyric : "";
  250. const tlyricsLines = data.tlyric ? data.tlyric.lyric : "";
  251.  
  252. currentLyrics = parseLyrics(lyricsLines);
  253. currentTLyrics = parseLyrics(tlyricsLines);
  254.  
  255. if (currentLyrics.length === 0) {
  256. showMessage('暂无歌词');
  257. return;
  258. }
  259.  
  260. renderLyrics();
  261. const audioPlayer = document.getElementById('apple-music-player');
  262. if (audioPlayer) {
  263. audioPlayer.dataset.songId = songId;
  264. }
  265. } else {
  266. showMessage('歌词获取失败');
  267. currentLyrics = [];
  268. currentTLyrics = [];
  269. }
  270. },
  271. onerror: function(err) {
  272. console.error(err);
  273. showMessage('歌词获取失败');
  274. currentLyrics = [];
  275. currentTLyrics = [];
  276. }
  277. });
  278. }
  279.  
  280. // 解析歌词文本(格式:[mm:ss.xxx]歌词内容)
  281. function parseLyrics(lyricsText) {
  282. return lyricsText.split('\n').filter(line => line.trim() !== '').map(line => {
  283. const matches = line.match(/\[(\d{2}):(\d{2})(?:\.(\d{1,3}))?\](.*)/);
  284. if (matches) {
  285. const minutes = parseInt(matches[1], 10);
  286. const seconds = parseInt(matches[2], 10);
  287. let milliseconds = matches[3] ? parseInt(matches[3], 10) : 0;
  288. if (milliseconds < 100 && milliseconds >= 10) {
  289. milliseconds *= 10;
  290. }
  291. const text = matches[4].trim();
  292. const totalSeconds = minutes * 60 + seconds + milliseconds / 1000;
  293. return { startTime: totalSeconds, text: text };
  294. }
  295. }).filter(Boolean);
  296. }
  297.  
  298. // 渲染歌词:每组歌词显示为两行(主歌词及对应翻译),每组之间有间隙
  299. function renderLyrics() {
  300. const lyricsContent = document.getElementById('lyrics-content');
  301. if (!lyricsContent) return;
  302. lyricsContent.innerHTML = '';
  303. const groupHeight = 70; // 每组固定高度(包括两行与间隙)
  304. currentLyrics.forEach((lyric, index) => {
  305. const groupDiv = document.createElement('div');
  306. groupDiv.className = 'lyric-group';
  307. groupDiv.dataset.index = index;
  308. groupDiv.style.height = groupHeight + 'px';
  309. groupDiv.style.marginBottom = '10px';
  310. // 主歌词行
  311. const mainDiv = document.createElement('div');
  312. mainDiv.className = 'main-lyric';
  313. mainDiv.innerText = lyric.text;
  314. mainDiv.style.fontSize = '28px';
  315. mainDiv.style.color = '#565656';
  316. // 匹配翻译歌词
  317. let translationText = "";
  318. if (currentTLyrics && currentTLyrics.length > 0) {
  319. const tLine = currentTLyrics.find(t => Math.abs(t.startTime - lyric.startTime) < 0.5);
  320. if (tLine) {
  321. translationText = tLine.text;
  322. }
  323. }
  324. const transDiv = document.createElement('div');
  325. transDiv.className = 'translation-lyric';
  326. transDiv.innerText = translationText;
  327. transDiv.style.fontSize = '20px';
  328. transDiv.style.color = '#888';
  329. transDiv.style.marginTop = '5px';
  330. groupDiv.appendChild(mainDiv);
  331. groupDiv.appendChild(transDiv);
  332. lyricsContent.appendChild(groupDiv);
  333. });
  334. }
  335.  
  336. // 当前歌词高亮
  337. function updateLyricScroll(currentTime) {
  338. if (currentLyrics.length === 0) return;
  339. let currentIndex = 0;
  340. for (let i = 0; i < currentLyrics.length; i++) {
  341. if (currentTime >= currentLyrics[i].startTime) {
  342. currentIndex = i;
  343. } else {
  344. break;
  345. }
  346. }
  347. const lyricsContent = document.getElementById('lyrics-content');
  348. if (lyricsContent === null) return;
  349. const groups = lyricsContent.getElementsByClassName('lyric-group');
  350. for (let i = 0; i < groups.length; i++) {
  351. const mainDiv = groups[i].querySelector('.main-lyric');
  352. const transDiv = groups[i].querySelector('.translation-lyric');
  353.  
  354. if (!mainDiv || !transDiv) continue;
  355.  
  356. if (i === currentIndex) {
  357. mainDiv.style.color = '#252525';
  358. mainDiv.style.fontWeight = 'bold';
  359. mainDiv.style.fontSize = '32px';
  360. mainDiv.style.filter = 'blur(0)';
  361. transDiv.style.filter = 'blur(0)';
  362. transDiv.style.color = '#353535';
  363. transDiv.style.fontWeight = 'bold';
  364. transDiv.style.fontSize = '24px';
  365. } else {
  366. mainDiv.style.color = '#565656';
  367. mainDiv.style.filter = 'blur(3px)';
  368. mainDiv.style.marginTop = '20px';
  369. mainDiv.style.fontWeight = 'normal';
  370. mainDiv.style.fontSize = '28px';
  371. transDiv.style.filter = 'blur(3px)';
  372. transDiv.style.color = '#888';
  373. transDiv.style.fontWeight = 'normal';
  374. transDiv.style.fontSize = '20px';
  375. }
  376. }
  377. // 计算滚动偏移(groupHeight + 下边距),不知道怎么调的,反正按照现在这样数值设置了看着还可以
  378. const groupHeight = 90;
  379. const container = document.getElementById('lyrics-display');
  380. const containerHeight = container.clientHeight;
  381. const offset = (currentIndex * groupHeight) - (containerHeight / 2 - groupHeight / 2) + 30;
  382. const lyricsContentDiv = document.getElementById('lyrics-content');
  383. lyricsContentDiv.style.transform = `translateY(-${offset}px)`;
  384. }
  385.  
  386. const lyricsDisplay = createLyricsDisplay();
  387.  
  388. // 更新滚动
  389. document.addEventListener('timeupdate', function(event) {
  390. const audioPlayer = event.target;
  391. if (audioPlayer.id === 'apple-music-player') {
  392. const startOffset = parseFloat(audioPlayer.dataset.startOffset) || 0;
  393. const effectiveTime = audioPlayer.currentTime - startOffset;
  394. updateLyricScroll(effectiveTime);
  395. }
  396. }, true);
  397.  
  398. // 每秒检测歌曲标题变化(切歌)
  399. setInterval(function() {
  400. const audioPlayer = document.getElementById('apple-music-player');
  401. if (audioPlayer) {
  402. let title = audioPlayer.title;
  403. if (title) {
  404. const secondDashIndex = title.indexOf('-', title.indexOf('-') + 1);
  405. if (secondDashIndex !== -1) {
  406. title = title.substring(0, secondDashIndex).trim();
  407. }
  408. if (title !== audioPlayer.dataset.lastTitle) {
  409. audioPlayer.dataset.lastTitle = title;
  410. audioPlayer.dataset.startOffset = audioPlayer.currentTime;
  411. const lyricsContent = document.getElementById('lyrics-content');
  412. if (lyricsContent) {
  413. lyricsContent.innerHTML = '';
  414. }
  415. getSongId();
  416. }
  417. }
  418. }
  419. }, 1000);
  420.  
  421. // 当前曲播放结束时也尝试重新获取歌词(适用于自动切换下一曲)
  422. const audioPlayer = document.getElementById('apple-music-player');
  423. if (audioPlayer) {
  424. audioPlayer.addEventListener('ended', function() {
  425. setTimeout(() => {
  426. audioPlayer.dataset.startOffset = audioPlayer.currentTime;
  427. getSongId();
  428. }, 500);
  429. });
  430. }
  431. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址