Apple Music 歌词增强

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name Apple Music 歌词增强
// @namespace https://github.com/akashiwest/AML-Enhancer
// @version 1.200
// @description 为网页版 Apple Music 提供翻译歌词,数据来源为网易云音乐
// @author Akashi
// @license GNU GPL 3.0
// @match https://*.music.apple.com/*
// @grant GM_xmlhttpRequest
// ==/UserScript==

(function() {
    'use strict';

    // 定义扩展和缩小时容器的高度
    const expandedHeight = '240px';
    const minimizedHeight = '130px';

    // 全局变量保存主歌词与翻译歌词数据
    let currentLyrics = [];
    let currentTLyrics = [];

    // 歌词时间偏移量(秒)
    let lyricsOffset = 0;

    // 当前歌曲信息
    let currentSongTitle = '';

    // 偏移存储对象 - 在实际环境中可以改为localStorage
    // 格式: { "歌名": 偏移值(秒) }
    let offsetStorage = {};

    // 存储/读取偏移值的函数
    function saveOffset(songTitle, offset) {
        if (!songTitle) return;

       try {
             let storedOffsets = JSON.parse(localStorage.getItem('lyricsOffsets') || '{}');
             storedOffsets[songTitle] = offset;
             localStorage.setItem('lyricsOffsets', JSON.stringify(storedOffsets));
         } catch (e) {
             console.error('保存歌词偏移失败:', e);
         }
    }

    function loadOffset(songTitle) {
        if (!songTitle) return 0;


         try {
             let storedOffsets = JSON.parse(localStorage.getItem('lyricsOffsets') || '{}');
             return storedOffsets[songTitle] || 0;
         } catch (e) {
             console.error('读取歌词偏移失败:', e);
             return 0;
         }
    }

    // 清除指定歌曲的偏移
    function clearOffset(songTitle) {
        if (!songTitle) return;

         try {
             let storedOffsets = JSON.parse(localStorage.getItem('lyricsOffsets') || '{}');
             delete storedOffsets[songTitle];
             localStorage.setItem('lyricsOffsets', JSON.stringify(storedOffsets));
         } catch (e) {
             console.error('清除歌词偏移失败:', e);
         }
    }

    // 创建固定容器(含内部内容容器)用于显示歌词
    function createLyricsDisplay() {
        const lyricsDiv = document.createElement('div');
        lyricsDiv.id = 'lyrics-display';
        // 默认缩小
        lyricsDiv.dataset.isMinimized = 'true';

        Object.assign(lyricsDiv.style, {
            position: 'fixed',
            right: '20px',
            top: '60px',
            width: '850px',
            height: minimizedHeight,
            overflow: 'hidden',
            borderRadius: '20px',
            backdropFilter: 'saturate(200%) blur(25px)',
            background: 'rgba(250,250,250,0.72)',
            zIndex: '9999',
            padding: '20px 30px',
            fontSize: '28px',
            color: '#565656',
            textAlign: 'center',
            boxShadow: '0 5px 30px rgba(0, 0, 0, 0.4)',
            fontWeight: 'bold',
            msOverflowStyle: 'none',
            scrollbarWidth: 'none',
            // 高度切换的动画效果,仅对 height 生效
            transition: 'height 0.3s ease'
        });

        // 内部容器,滚动时带平滑动画
        const lyricsContent = document.createElement('div');
        lyricsContent.id = 'lyrics-content';
        lyricsContent.style.transition = 'transform 0.3s ease-out';
        lyricsContent.style.transform = 'translateY(0)';
        lyricsDiv.appendChild(lyricsContent);

        const defaultGroup = document.createElement('div');
        defaultGroup.className = 'lyric-group';
        defaultGroup.style.height = '70px';
        defaultGroup.style.marginBottom = '10px';

        const defaultText = document.createElement('div');
        defaultText.className = 'main-lyric';
        defaultText.innerText = 'Apple Music 歌词翻译 v1.2';
        defaultText.style.fontSize = '32px';
        defaultText.style.color = '#252525';
        defaultText.style.fontWeight = 'bold';
        defaultText.style.marginTop = '25px';

        defaultGroup.appendChild(defaultText);
        lyricsContent.appendChild(defaultGroup);
        lyricsDiv.appendChild(lyricsContent);

        // 创建右下角按钮容器
        const rightButtonContainer = document.createElement('div');
        Object.assign(rightButtonContainer.style, {
            position: 'absolute',
            bottom: '10px',
            right: '10px',
            display: 'flex',
            gap: '5px',
            alignItems: 'center'
        });

        // 创建左下角按钮容器
        const leftButtonContainer = document.createElement('div');
        Object.assign(leftButtonContainer.style, {
            position: 'absolute',
            bottom: '10px',
            left: '10px',
            display: 'flex',
            gap: '5px',
            alignItems: 'center'
        });

        // 减号按钮
        const minusButton = document.createElement('button');
        minusButton.id = 'minus-button';
        minusButton.innerText = '−';
        Object.assign(minusButton.style, {
            padding: '5px 8px',
            fontSize: '16px',
            fontWeight: 'bold',
            border: 'none',
            borderRadius: '15px',
            background: '#ddd',
            cursor: 'pointer',
            zIndex: '10000',
            opacity: '0.8',
            minWidth: '30px'
        });

        // 偏移显示按钮
        const offsetButton = document.createElement('button');
        offsetButton.id = 'offset-button';
        offsetButton.innerText = '+0.0s';
        Object.assign(offsetButton.style, {
            padding: '5px 10px',
            fontSize: '12px',
            border: 'none',
            borderRadius: '15px',
            background: '#ddd',
            cursor: 'pointer',
            zIndex: '10000',
            opacity: '0.8',
            minWidth: '50px'
        });

        // 加号按钮
        const plusButton = document.createElement('button');
        plusButton.id = 'plus-button';
        plusButton.innerText = '+';
        Object.assign(plusButton.style, {
            padding: '5px 8px',
            fontSize: '16px',
            fontWeight: 'bold',
            border: 'none',
            borderRadius: '15px',
            background: '#ddd',
            cursor: 'pointer',
            zIndex: '10000',
            opacity: '0.8',
            minWidth: '30px'
        });

        // 切换按钮
        const toggleButton = document.createElement('button');
        toggleButton.id = 'toggle-size-button';
        toggleButton.innerText = '放大';
        Object.assign(toggleButton.style, {
            padding: '5px 10px',
            fontSize: '14px',
            border: 'none',
            borderRadius: '15px',
            background: '#ddd',
            cursor: 'pointer',
            zIndex: '10000',
            opacity: '0.8'
        });

        // 信息按钮
        const infoButton = document.createElement('button');
        infoButton.id = 'info-button';
        infoButton.innerText = 'O';
        Object.assign(infoButton.style, {
            padding: '5px 8px',
            fontSize: '14px',
            border: 'none',
            borderRadius: '15px',
            background: '#ddd',
            cursor: 'pointer',
            zIndex: '10000',
            opacity: '0.3'
        });

        // 更新偏移显示
        function updateOffsetDisplay() {
            const offsetValue = lyricsOffset >= 0 ? `+${lyricsOffset.toFixed(1)}s` : `${lyricsOffset.toFixed(1)}s`;
            offsetButton.innerText = offsetValue;

            // 自动保存当前歌曲的偏移值
            if (currentSongTitle) {
                saveOffset(currentSongTitle, lyricsOffset);
            }
        }

        // 减号按钮事件
        minusButton.addEventListener('click', function(e) {
            e.stopPropagation();
            lyricsOffset -= 0.2;
            updateOffsetDisplay();
        });

        // 偏移按钮事件(点击归零并清除存储)
        offsetButton.addEventListener('click', function(e) {
            e.stopPropagation();
            lyricsOffset = 0;
            updateOffsetDisplay();

            // 清除当前歌曲的存储偏移
            if (currentSongTitle) {
                clearOffset(currentSongTitle);
            }
        });

        // 加号按钮事件
        plusButton.addEventListener('click', function(e) {
            e.stopPropagation();
            lyricsOffset += 0.2;
            updateOffsetDisplay();
        });

        // 切换按钮事件
        toggleButton.addEventListener('click', function(e) {
            e.stopPropagation();
            if (lyricsDiv.dataset.isMinimized === 'true') {
                // 扩展
                lyricsDiv.style.height = expandedHeight;
                lyricsDiv.dataset.isMinimized = 'false';
                toggleButton.innerText = '缩小';
            } else {
                // 缩小
                lyricsDiv.style.height = minimizedHeight;
                lyricsDiv.dataset.isMinimized = 'true';
                toggleButton.innerText = '放大';
            }
        });

        // 信息按钮事件
        infoButton.addEventListener('click', function(e) {
            e.stopPropagation();
            window.open('https://imakashi.com/blog/archives/AML_Enhancer.html', '_blank');
        });

        // 将按钮添加到对应的容器
        rightButtonContainer.appendChild(minusButton);
        rightButtonContainer.appendChild(offsetButton);
        rightButtonContainer.appendChild(plusButton);
        rightButtonContainer.appendChild(toggleButton);

        leftButtonContainer.appendChild(infoButton);

        lyricsDiv.appendChild(rightButtonContainer);
        lyricsDiv.appendChild(leftButtonContainer);

        // 拖拽功能
        lyricsDiv.onmousedown = dragMouseDown;

        let pos3 = 0, pos4 = 0;

        function dragMouseDown(e) {
            // 点击按钮不触发拖拽
            if (e.target === toggleButton || e.target === infoButton ||
                e.target === minusButton || e.target === offsetButton || e.target === plusButton) return;
            e.preventDefault();
            pos3 = e.clientX;
            pos4 = e.clientY;
            document.onmouseup = closeDragElement;
            document.onmousemove = elementDrag;
        }

        function elementDrag(e) {
            e.preventDefault();
            const pos1 = pos3 - e.clientX;
            const pos2 = pos4 - e.clientY;
            pos3 = e.clientX;
            pos4 = e.clientY;
            lyricsDiv.style.top = `${lyricsDiv.offsetTop - pos2}px`;
            lyricsDiv.style.left = `${lyricsDiv.offsetLeft - pos1}px`;
        }

        function closeDragElement() {
            document.onmouseup = null;
            document.onmousemove = null;
        }

        document.body.appendChild(lyricsDiv);
        return lyricsDiv;
    }

    // 根据播放器 title 获取歌曲ID,并调用歌词接口
    function getSongId() {
        const audioPlayer = document.getElementById('apple-music-player');
        if (!audioPlayer) {
            console.log('当前页面未找到音频播放器');
            return;
        }

        let title = audioPlayer.title;
        // 取标题中第一个"-"前面的部分(可根据实际情况调整)
        const secondDashIndex = title.indexOf('-', title.indexOf('-') + 1);
        if (secondDashIndex !== -1) {
            title = title.substring(0, secondDashIndex).trim();
        }

        // 更新当前歌曲标题并加载对应的偏移值
        currentSongTitle = title;
        lyricsOffset = loadOffset(currentSongTitle);

        // 更新显示
        const offsetButton = document.getElementById('offset-button');
        if (offsetButton) {
            const offsetValue = lyricsOffset >= 0 ? `+${lyricsOffset.toFixed(1)}s` : `${lyricsOffset.toFixed(1)}s`;
            offsetButton.innerText = offsetValue;
        }

        showMessage(title);

        const apiUrl = `https://music.163.com/api/search/pc?s=${encodeURIComponent(title)}&offset=0&limit=1&type=1`;

        GM_xmlhttpRequest({
            method: "GET",
            url: apiUrl,
            responseType: "json",
            onload: function(response) {
                if (response.status === 200) {
                    const data = response.response;
                    if (data.result && data.result.songs && data.result.songs.length > 0) {
                        const firstSongId = data.result.songs[0].id;
                        getLyrics(firstSongId);
                        console.log(apiUrl, 'ID - ' + firstSongId);
                    } else {
                        showMessage("未找到歌曲");
                    }
                } else {
                    showMessage("请求失败");
                }
            },
            onerror: function() {
                console.error("未知错误");
            }
        });
    }

    // 提示信息
    function showMessage(msg) {
        const lyricsContent = document.getElementById('lyrics-content');
        if (lyricsContent) {
            const messageGroup = document.createElement('div');
            messageGroup.className = 'lyric-group';
            messageGroup.style.height = '70px';
            messageGroup.style.marginBottom = '10px';

            const messageText = document.createElement('div');
            messageText.className = 'main-lyric';
            messageText.innerText = msg;
            messageText.style.fontSize = '32px';
            messageText.style.color = '#252525';
            messageText.style.fontWeight = 'bold';
            messageText.style.filter = 'blur(0) !important';

            // 添加容器样式以确保垂直居中
            messageGroup.style.display = 'flex';
            messageGroup.style.alignItems = 'center';
            messageGroup.style.justifyContent = 'center';
            messageGroup.style.height = '100%';
            messageGroup.style.marginTop = '25px';

            messageGroup.appendChild(messageText);
            lyricsContent.innerHTML = '';
            lyricsContent.appendChild(messageGroup);
        }
    }

    // 获取歌词(同时获取主歌词和翻译歌词)并解析后渲染
    function getLyrics(songId) {
        const apiUrl = `https://music.163.com/api/song/lyric?lv=1&kv=1&tv=-1&id=${songId}`;
        showMessage('歌词正在加载中 ...');

        GM_xmlhttpRequest({
            method: "GET",
            url: apiUrl,
            responseType: "json",
            onload: function(response) {
                if (response.status === 200) {
                    const data = response.response;
                    if (!data || (!data.lrc && !data.tlyric)) {
                        showMessage('未找到匹配歌词');
                        currentLyrics = [];
                        currentTLyrics = [];
                        return;
                    }

                    const lyricsLines = data.lrc ? data.lrc.lyric : "";
                    const tlyricsLines = data.tlyric ? data.tlyric.lyric : "";

                    currentLyrics = parseLyrics(lyricsLines);
                    currentTLyrics = parseLyrics(tlyricsLines);

                    if (currentLyrics.length === 0) {
                        showMessage('暂无歌词');
                        return;
                    }

                    renderLyrics();

                    const audioPlayer = document.getElementById('apple-music-player');
                    if (audioPlayer) {
                        audioPlayer.dataset.songId = songId;
                    }
                } else {
                    showMessage('歌词获取失败');
                    currentLyrics = [];
                    currentTLyrics = [];
                }
            },
            onerror: function(err) {
                console.error(err);
                showMessage('歌词获取失败');
                currentLyrics = [];
                currentTLyrics = [];
            }
        });
    }

    // 解析歌词文本(格式:[mm:ss.xxx]歌词内容)
    function parseLyrics(lyricsText) {
        return lyricsText.split('\n').filter(line => line.trim() !== '').map(line => {
            const matches = line.match(/\[(\d{2}):(\d{2})(?:\.(\d{1,3}))?\](.*)/);
            if (matches) {
                const minutes = parseInt(matches[1], 10);
                const seconds = parseInt(matches[2], 10);
                let milliseconds = matches[3] ? parseInt(matches[3], 10) : 0;

                if (milliseconds < 100 && milliseconds >= 10) {
                    milliseconds *= 10;
                }

                const text = matches[4].trim();
                const totalSeconds = minutes * 60 + seconds + milliseconds / 1000;

                return { startTime: totalSeconds, text: text };
            }
        }).filter(Boolean);
    }

    // 渲染歌词:每组歌词显示为两行(主歌词及对应翻译),每组之间有间隙
    function renderLyrics() {
        const lyricsContent = document.getElementById('lyrics-content');
        if (!lyricsContent) return;

        lyricsContent.innerHTML = '';
        const groupHeight = 70; // 每组固定高度(包括两行与间隙)

        currentLyrics.forEach((lyric, index) => {
            const groupDiv = document.createElement('div');
            groupDiv.className = 'lyric-group';
            groupDiv.dataset.index = index;
            groupDiv.style.height = groupHeight + 'px';
            groupDiv.style.marginBottom = '10px';

            // 主歌词行
            const mainDiv = document.createElement('div');
            mainDiv.className = 'main-lyric';
            mainDiv.innerText = lyric.text;
            mainDiv.style.fontSize = '28px';
            mainDiv.style.color = '#565656';

            // 匹配翻译歌词
            let translationText = "";
            if (currentTLyrics && currentTLyrics.length > 0) {
                const tLine = currentTLyrics.find(t => Math.abs(t.startTime - lyric.startTime) < 0.5);
                if (tLine) {
                    translationText = tLine.text;
                }
            }

            const transDiv = document.createElement('div');
            transDiv.className = 'translation-lyric';
            transDiv.innerText = translationText;
            transDiv.style.fontSize = '20px';
            transDiv.style.color = '#888';
            transDiv.style.marginTop = '5px';

            groupDiv.appendChild(mainDiv);
            groupDiv.appendChild(transDiv);
            lyricsContent.appendChild(groupDiv);
        });
    }

    // 当前歌词高亮
    function updateLyricScroll(currentTime) {
        if (currentLyrics.length === 0) return;

        // 应用时间偏移
        const adjustedTime = currentTime + lyricsOffset;

        let currentIndex = 0;
        for (let i = 0; i < currentLyrics.length; i++) {
            if (adjustedTime >= currentLyrics[i].startTime) {
                currentIndex = i;
            } else {
                break;
            }
        }

        const lyricsContent = document.getElementById('lyrics-content');
        if (lyricsContent === null) return;

        const groups = lyricsContent.getElementsByClassName('lyric-group');
        for (let i = 0; i < groups.length; i++) {
            const mainDiv = groups[i].querySelector('.main-lyric');
            const transDiv = groups[i].querySelector('.translation-lyric');
            if (!mainDiv || !transDiv) continue;

            if (i === currentIndex) {
                mainDiv.style.color = '#252525';
                mainDiv.style.fontWeight = 'bold';
                mainDiv.style.fontSize = '32px';
                mainDiv.style.filter = 'blur(0)';
                transDiv.style.filter = 'blur(0)';
                transDiv.style.color = '#353535';
                transDiv.style.fontWeight = 'bold';
                transDiv.style.fontSize = '24px';
            } else {
                mainDiv.style.color = '#565656';
                mainDiv.style.filter = 'blur(3px)';
                mainDiv.style.marginTop = '20px';
                mainDiv.style.fontWeight = 'normal';
                mainDiv.style.fontSize = '28px';
                transDiv.style.filter = 'blur(3px)';
                transDiv.style.color = '#888';
                transDiv.style.fontWeight = 'normal';
                transDiv.style.fontSize = '20px';
            }
        }

        // 计算滚动偏移(groupHeight + 下边距),不知道怎么调的,反正按照现在这样数值设置了看着还可以
        const groupHeight = 90;
        const container = document.getElementById('lyrics-display');
        const containerHeight = container.clientHeight;
        const offset = (currentIndex * groupHeight) - (containerHeight / 2 - groupHeight / 2) + 30;

        const lyricsContentDiv = document.getElementById('lyrics-content');
        lyricsContentDiv.style.transform = `translateY(-${offset}px)`;
    }

    const lyricsDisplay = createLyricsDisplay();

    // 更新滚动
    document.addEventListener('timeupdate', function(event) {
        const audioPlayer = event.target;
        if (audioPlayer.id === 'apple-music-player') {
            const startOffset = parseFloat(audioPlayer.dataset.startOffset) || 0;
            const effectiveTime = audioPlayer.currentTime - startOffset;
            updateLyricScroll(effectiveTime);
        }
    }, true);

    // 每秒检测歌曲标题变化(切歌)
    setInterval(function() {
        const audioPlayer = document.getElementById('apple-music-player');
        if (audioPlayer) {
            let title = audioPlayer.title;
            if (title) {
                const secondDashIndex = title.indexOf('-', title.indexOf('-') + 1);
                if (secondDashIndex !== -1) {
                    title = title.substring(0, secondDashIndex).trim();
                }

                if (title !== audioPlayer.dataset.lastTitle) {
                    audioPlayer.dataset.lastTitle = title;
                    audioPlayer.dataset.startOffset = audioPlayer.currentTime;

                    const lyricsContent = document.getElementById('lyrics-content');
                    if (lyricsContent) {
                        lyricsContent.innerHTML = '';
                    }
                    getSongId();
                }
            }
        }
    }, 1000);

    // 当前曲播放结束时也尝试重新获取歌词(适用于自动切换下一曲)
    const audioPlayer = document.getElementById('apple-music-player');
    if (audioPlayer) {
        audioPlayer.addEventListener('ended', function() {
            setTimeout(() => {
                audioPlayer.dataset.startOffset = audioPlayer.currentTime;
                getSongId();
            }, 500);
        });
    }
})();