您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Enhances the native lyrics UI on dab.yeet.su with a custom, stable lyric stream.
// ==UserScript== // @name DabLyrics // @namespace https://github.com/ahnaf0014/DabLyrics // @version 1.0.0 // @description Enhances the native lyrics UI on dab.yeet.su with a custom, stable lyric stream. // @author Ahnaf // @match https://dab.yeet.su/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @connect lyrics-api.boidu.dev // @connect lrclib.net // @connect translate.googleapis.com // @license MIT // ==/UserScript== (function () { 'use strict'; // --- SETTINGS --- const SETTINGS = { language: "en", checkInterval: 5000, renderInterval: 100, timingOffset: -0.2, lyricsDelay: 280 }; // --- SELECTORS & IDs --- const PLAYER_BAR_SELECTOR = "body > div > div.rounded-lg.border.text-card-foreground.bg-zinc-900\\/80.backdrop-blur-md.border-zinc-700\\/30.fixed.bottom-0.left-0.right-0.z-40"; const MY_NATIVE_VIEW_ID = 'bl-native-fullscreen-container'; const NATIVE_LYRICS_VIEWPORT_ID = 'bl-native-viewport'; const NATIVE_LYRICS_TAPE_ID = 'bl-native-tape'; const FLOATING_UI_ID = 'better-lyrics-floater'; const FLOATING_CONTENT_ID = 'bl-content'; const FLOATING_HEADER_ID = 'bl-header'; const FLOATING_COLLAPSE_BTN_ID = 'bl-collapse-btn'; const FLOATING_RESIZE_HANDLE_ID = 'bl-resize-handle'; const NATIVE_LINE_CLASS_BASE = ['py-2', 'transition-all', 'duration-500', 'ease-in-out', 'text-center']; const NATIVE_LINE_CLASS_INACTIVE = ['text-zinc-400/80', 'text-xl']; const NATIVE_LINE_CLASS_ACTIVE = ['text-white', 'font-medium', 'text-2xl']; // --- STATE MANAGEMENT --- let lastSong = ''; let isFetching = false; let currentLyrics = []; const state = { cachedLyrics: {}, isFloaterCollapsed: GM_getValue('bl_floater_collapsed', false), floaterPos: GM_getValue('bl_floater_pos', { x: 20, y: 20 }), floaterSize: GM_getValue('bl_floater_size', { w: 400, h: 350 }) }; const renderState = { lyricElements: { native: [], floating: [] }, lastActiveIndex: { native: -1, floating: -1 } }; // --- DATA & PLAYER HELPERS --- function parseLRC(text) { return text.split("\n").map(line => { const match = line.match(/\[(\d+):(\d+)(?:[.,](\d+))?]/); if (!match) return null; const time = parseInt(match[1]) * 60 + parseInt(match[2]) + parseInt(match[3] || '0') / 1000; const content = line.replace(/\[.*?]/g, '').trim(); return content ? { time, text: content } : null; }).filter(Boolean).sort((a, b) => a.time - b.time); } async function fetchLyrics(artist, title) { const cacheKey = `${artist} - ${title}`; if (state.cachedLyrics[cacheKey]) return state.cachedLyrics[cacheKey]; const urls = [`https://lrclib.net/api/get?artist_name=${encodeURIComponent(artist)}&track_name=${encodeURIComponent(title)}`, `https://lyrics-api.boidu.dev/?artist=${encodeURIComponent(artist)}&title=${encodeURIComponent(title)}`]; for (const url of urls) { try { const res = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url, timeout: 10000, onload: r => resolve(JSON.parse(r.responseText)), onerror: reject, ontimeout: reject }); }); if (res?.syncedLyrics) { const parsed = parseLRC(res.syncedLyrics); if (parsed.length > 0) { state.cachedLyrics[cacheKey] = parsed; return parsed; } } } catch (e) { /* Fetch failed, try next source */ } } return []; } async function translateLyrics(lyrics, toLang) { if (toLang === 'en' || !lyrics.length) return lyrics; const text = lyrics.map(l => l.text).join("\n"); const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${toLang}&dt=t&q=${encodeURIComponent(text)}`; return new Promise((resolve) => { GM_xmlhttpRequest({ method: "GET", url, timeout: 15000, onload: res => { try { const translatedSentences = JSON.parse(res.responseText)[0]; const translatedLines = translatedSentences.map(t => t[0]).join('').split('\n'); resolve(lyrics.map((line, i) => ({ ...line, text: translatedLines[i] || line.text }))); } catch { resolve(lyrics); } }, onerror: () => resolve(lyrics), ontimeout: () => resolve(lyrics) }); }); } function parseTimeToSeconds(timeStr) { if (!timeStr || !timeStr.includes(':')) return 0; const parts = timeStr.split(':').map(Number); if (parts.length === 2) return parts[0] * 60 + parts[1]; if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2]; return 0; } function getNowPlayingInfo() { const playerBar = document.querySelector(PLAYER_BAR_SELECTOR); if (!playerBar) return null; const titleElem = playerBar.querySelector("div.min-w-0.flex-1 > div:nth-child(1) > h3"); const artistElem = playerBar.querySelector("div.min-w-0.flex-1 > p"); if (!titleElem || !artistElem) return null; return { title: titleElem.textContent.trim(), artist: artistElem.textContent.trim() }; } function getCurrentTime() { const playerBar = document.querySelector(PLAYER_BAR_SELECTOR); const mainTimeElem = playerBar ? playerBar.querySelector("div.flex-1.w-full > div.flex.items-center.gap-2 > span:nth-child(1)") : null; const lyricsDelayInSeconds = SETTINGS.lyricsDelay / 1000; return mainTimeElem ? parseTimeToSeconds(mainTimeElem.textContent) + SETTINGS.timingOffset + lyricsDelayInSeconds : 0; } function getTotalTime() { const playerBar = document.querySelector(PLAYER_BAR_SELECTOR); const totalTimeElem = playerBar ? playerBar.querySelector("div.flex-1.w-full > div.flex.items-center.gap-2 > span:last-child") : null; return totalTimeElem ? parseTimeToSeconds(totalTimeElem.textContent) : 0; } // --- UI & STYLES --- function injectUIManager() { const css = ` #${MY_NATIVE_VIEW_ID} { display: none; position: fixed; inset: 0; z-index: 10000; background: linear-gradient(to bottom, rgba(39, 39, 42, 0.98), #000, rgba(39, 39, 42, 0.98)); flex-direction: column; padding: 1rem; } #${NATIVE_LYRICS_VIEWPORT_ID} { position: relative; flex-grow: 1; overflow: hidden; } #${NATIVE_LYRICS_TAPE_ID} { position: absolute; width: 100%; top: 50%; left: 0; transition: transform 500ms cubic-bezier(0.25, 0.46, 0.45, 0.94); } #${NATIVE_LYRICS_TAPE_ID} > div { transition: opacity 500ms, transform 500ms; } #${FLOATING_UI_ID} { position: fixed; z-index: 9999; background-color: rgba(20, 20, 20, 0.85); backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 12px; box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); display: flex; flex-direction: column; font-family: sans-serif; min-width: 250px; min-height: 120px; max-width: 90vw; max-height: 80vh; } #${FLOATING_UI_ID}.collapsed { min-height: 50px; height: 50px !important; width: 150px !important; overflow: hidden; } #${FLOATING_HEADER_ID} { padding: 8px 12px; cursor: move; background-color: rgba(255, 255, 255, 0.1); border-bottom: 1px solid rgba(255, 255, 255, 0.1); display: flex; justify-content: space-between; align-items: center; border-top-left-radius: 12px; border-top-right-radius: 12px; flex-shrink: 0; } #${FLOATING_HEADER_ID} span { color: #eee; font-weight: bold; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } #${FLOATING_COLLAPSE_BTN_ID} { cursor: pointer; background: none; border: none; color: #ddd; font-size: 20px; line-height: 1; padding: 0 4px; } #${FLOATING_CONTENT_ID} { flex-grow: 1; overflow-y: auto; padding: 10px; display: flex; flex-direction: column; gap: 8px; } #${FLOATING_UI_ID}.collapsed #${FLOATING_CONTENT_ID} { display: none; } #${FLOATING_CONTENT_ID} p { margin: 0; text-align: center; transition: all 0.3s ease; font-size: 16px; color: #aaa; } #${FLOATING_CONTENT_ID} p.active { color: #fff; font-weight: bold; font-size: 18px; } #${FLOATING_CONTENT_ID} .bl-message, .bl-native-message { color: #ccc; font-size: 16px; text-align: center; padding: 20px; } .bl-native-message { font-size: 1.25rem; padding: 2rem; } #${FLOATING_RESIZE_HANDLE_ID} { position: absolute; right: 0; bottom: 0; width: 20px; height: 20px; cursor: se-resize; z-index: 10000; } #${FLOATING_UI_ID}.collapsed #${FLOATING_RESIZE_HANDLE_ID} { display: none; } `; const style = document.createElement('style'); style.textContent = css; document.head.appendChild(style); const floater = document.createElement('div'); floater.id = FLOATING_UI_ID; floater.innerHTML = `<div id="${FLOATING_HEADER_ID}"><span>DabLyrics</span><button id="${FLOATING_COLLAPSE_BTN_ID}">_</button></div><div id="${FLOATING_CONTENT_ID}" class="custom-scrollbar"></div><div id="${FLOATING_RESIZE_HANDLE_ID}"></div>`; document.body.appendChild(floater); const myNativeView = document.createElement('div'); myNativeView.id = MY_NATIVE_VIEW_ID; document.body.appendChild(myNativeView); floater.style.left = `${state.floaterPos.x}px`; floater.style.top = `${state.floaterPos.y}px`; floater.style.width = `${state.floaterSize.w}px`; floater.style.height = `${state.floaterSize.h}px`; if (state.isFloaterCollapsed) { floater.classList.add('collapsed'); floater.querySelector(`#${FLOATING_COLLAPSE_BTN_ID}`).textContent = '□'; } makeDraggable(floater, floater.querySelector(`#${FLOATING_HEADER_ID}`)); makeResizable(floater, floater.querySelector(`#${FLOATING_RESIZE_HANDLE_ID}`)); floater.querySelector(`#${FLOATING_COLLAPSE_BTN_ID}`).addEventListener('click', toggleFloaterCollapse); } function makeDraggable(element, handle) { let pos3 = 0, pos4 = 0; handle.onmousedown = e => { e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = closeDragElement; document.onmousemove = elementDrag; }; const elementDrag = e => { e.preventDefault(); element.style.top = (element.offsetTop - (pos4 - e.clientY)) + "px"; element.style.left = (element.offsetLeft - (pos3 - e.clientX)) + "px"; pos3 = e.clientX; pos4 = e.clientY; }; const closeDragElement = () => { document.onmouseup = null; document.onmousemove = null; state.floaterPos = { x: element.offsetLeft, y: element.offsetTop }; GM_setValue('bl_floater_pos', state.floaterPos); }; } function makeResizable(element, handle) { let initialW, initialH, initialX, initialY; handle.onmousedown = e => { e.preventDefault(); e.stopPropagation(); initialW = element.offsetWidth; initialH = element.offsetHeight; initialX = e.clientX; initialY = e.clientY; document.onmousemove = elementResize; document.onmouseup = stopResize; }; const elementResize = e => { element.style.width = (initialW + (e.clientX - initialX)) + 'px'; element.style.height = (initialH + (e.clientY - initialY)) + 'px'; }; const stopResize = () => { document.onmousemove = null; document.onmouseup = null; state.floaterSize = { w: element.offsetWidth, h: element.offsetHeight }; GM_setValue('bl_floater_size', state.floaterSize); }; } function toggleFloaterCollapse(e) { e.stopPropagation(); state.isFloaterCollapsed = !state.isFloaterCollapsed; GM_setValue('bl_floater_collapsed', state.isFloaterCollapsed); const floater = document.getElementById(FLOATING_UI_ID); const btn = document.getElementById(FLOATING_COLLAPSE_BTN_ID); if (state.isFloaterCollapsed) { floater.classList.add('collapsed'); btn.textContent = '□'; } else { floater.classList.remove('collapsed'); btn.textContent = '_'; } } // --- CORE LOGIC --- function openMyNativeView() { const view = document.getElementById(MY_NATIVE_VIEW_ID); if (!view) return; view.innerHTML = ''; const container = document.createElement('div'); container.id = NATIVE_LYRICS_VIEWPORT_ID; const closeButton = document.createElement('button'); closeButton.textContent = '▼'; closeButton.style.cssText = 'position: absolute; top: 1rem; right: 1.5rem; font-size: 1.5rem; color: white; background: none; border: none; cursor: pointer; z-index: 1;'; closeButton.onclick = closeMyNativeView; view.appendChild(closeButton); view.appendChild(container); buildLyricsDOM(container, currentLyrics, false); view.style.display = 'flex'; const nativeLyricsView = document.querySelector('.flex-1.overflow-y-auto.pr-2.custom-scrollbar'); if (nativeLyricsView) { nativeLyricsView.style.display = 'none'; } updateUI(); } function closeMyNativeView() { const view = document.getElementById(MY_NATIVE_VIEW_ID); if (view) view.style.display = 'none'; const nativeLyricsView = document.querySelector('.flex-1.overflow-y-auto.pr-2.custom-scrollbar'); if (nativeLyricsView) { nativeLyricsView.style.display = ''; } } function buildLyricsDOM(container, lyrics, isFloating) { container.innerHTML = ''; const uiKey = isFloating ? 'floating' : 'native'; renderState.lyricElements[uiKey] = []; renderState.lastActiveIndex[uiKey] = -1; const messageClass = isFloating ? 'bl-message' : 'bl-native-message'; if (isFetching) { container.innerHTML = `<div class="${messageClass}">🎵 Fetching lyrics...</div>`; return; } if (!lyrics || lyrics.length === 0) { container.innerHTML = `<div class="${messageClass}">⚠️ No synced lyrics found for <b>${lastSong || 'this song'}</b>.</div>`; return; } const songEndTime = getTotalTime(); const finalTime = songEndTime > 0 ? songEndTime : 99999; const displayLyrics = [{ time: 0, text: '...' }, ...lyrics, { time: finalTime, text: '(end)' }]; if (isFloating) { displayLyrics.forEach(line => { const lineElem = document.createElement('p'); lineElem.textContent = line.text; container.appendChild(lineElem); renderState.lyricElements[uiKey].push(lineElem); }); } else { const tape = document.createElement('div'); tape.id = NATIVE_LYRICS_TAPE_ID; container.appendChild(tape); displayLyrics.forEach(line => { const lineElem = document.createElement('div'); lineElem.className = [...NATIVE_LINE_CLASS_BASE, ...NATIVE_LINE_CLASS_INACTIVE].join(' '); lineElem.innerHTML = `<p>${line.text}</p>`; tape.appendChild(lineElem); renderState.lyricElements[uiKey].push(lineElem); }); } } async function mainLoop() { const info = getNowPlayingInfo(); if (!info) return; const songKey = `${info.artist} - ${info.title}`; if (songKey === lastSong || isFetching) return; lastSong = songKey; isFetching = true; currentLyrics = []; renderState.lyricElements['floating'] = []; document.querySelector(`#${FLOATING_HEADER_ID} span`).textContent = songKey; updateUI(); try { let lyrics = await fetchLyrics(info.artist, info.title); if (lyrics.length > 0 && SETTINGS.language !== 'en') { lyrics = await translateLyrics(lyrics, SETTINGS.language); } currentLyrics = lyrics; } catch (e) { console.error("DabLyrics: Lyrics fetch failed:", e); currentLyrics = []; } finally { isFetching = false; updateUI(); } } function updateUI() { const myNativeView = document.getElementById(MY_NATIVE_VIEW_ID); const isMyNativeViewVisible = myNativeView && getComputedStyle(myNativeView).display !== 'none'; const floater = document.getElementById(FLOATING_UI_ID); if (floater) { isMyNativeViewVisible ? floater.style.display = 'none' : floater.style.display = 'flex'; } const isFloating = !isMyNativeViewVisible; const uiKey = isFloating ? 'floating' : 'native'; const container = isFloating ? document.getElementById(FLOATING_CONTENT_ID) : document.getElementById(NATIVE_LYRICS_VIEWPORT_ID); if (!container) return; if (renderState.lyricElements[uiKey].length === 0) { buildLyricsDOM(container, currentLyrics, isFloating); } if (isFetching || !currentLyrics || currentLyrics.length === 0) { return; } const songEndTime = getTotalTime(); const finalTime = songEndTime > 0 ? songEndTime : 99999; const displayLyrics = [{ time: 0, text: '...' }, ...currentLyrics, { time: finalTime, text: '(end)' }]; const currentTime = getCurrentTime(); let newActiveIndex = displayLyrics.findIndex((line, i) => { const nextLine = displayLyrics[i + 1]; return currentTime >= line.time && (!nextLine || currentTime < nextLine.time); }); const lastIndex = renderState.lastActiveIndex[uiKey]; if (newActiveIndex !== lastIndex) { const elements = renderState.lyricElements[uiKey]; if (!elements || elements.length === 0 || !elements[newActiveIndex]) return; if (isFloating) { const oldElem = elements[lastIndex]; const newElem = elements[newActiveIndex]; if (oldElem) oldElem.classList.remove('active'); if (newElem) { newElem.classList.add('active'); newElem.scrollIntoView({ behavior: 'smooth', block: 'center' }); } } else { const tape = document.getElementById(NATIVE_LYRICS_TAPE_ID); const activeLineElem = elements[newActiveIndex]; if (tape && activeLineElem) { const offset = -activeLineElem.offsetTop; tape.style.transform = `translateY(${offset}px)`; elements.forEach((line, index) => { const distance = Math.abs(index - newActiveIndex); line.style.opacity = Math.max(0, 1 - distance * 0.35); line.style.transform = `scale(${Math.max(0.8, 1 - distance * 0.05)})`; line.className = (distance === 0) ? [...NATIVE_LINE_CLASS_BASE, ...NATIVE_LINE_CLASS_ACTIVE].join(' ') : [...NATIVE_LINE_CLASS_BASE, ...NATIVE_LINE_CLASS_INACTIVE].join(' '); }); } } renderState.lastActiveIndex[uiKey] = newActiveIndex; } } // --- BUTTON HIJACKING --- function hijackLyricsButton() { const buttons = document.querySelectorAll('button'); let targetButton = null; buttons.forEach(button => { if (button.getAttribute('aria-label')?.toLowerCase().includes('show lyrics fullscreen')) { targetButton = button; } }); if (targetButton && !targetButton.dataset.hijacked) { targetButton.dataset.hijacked = 'true'; targetButton.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); openMyNativeView(); }, true); } } // --- INITIALIZATION --- function init() { injectUIManager(); const lyricsButtonObserver = new MutationObserver(hijackLyricsButton); lyricsButtonObserver.observe(document.body, { childList: true, subtree: true }); hijackLyricsButton(); setInterval(mainLoop, SETTINGS.checkInterval); setInterval(updateUI, SETTINGS.renderInterval); mainLoop(); } const readyStateCheck = setInterval(() => { if (document.readyState === "complete") { clearInterval(readyStateCheck); init(); } }, 100); })();