您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
可更換 marumaru 片源為你指定的 youtube 影片,並且提供更精確的校時
当前为
// ==UserScript== // @name marumaru 更換影片 與 歌詞播放延遲 // @namespace Anong0u0 // @version 0.3.1 // @description 可更換 marumaru 片源為你指定的 youtube 影片,並且提供更精確的校時 // @author Anong0u0 // @match https://www.marumaru-x.com/*/* // @icon https://www.google.com/s2/favicons?sz=64&domain=marumaru-x.com // @grant GM_setValue // @grant GM_getValue // @run-at document-start // @license Beerware // ==/UserScript== const delay = (ms = 0) => new Promise((r)=>{setTimeout(r, ms)}) const waitElementLoad = (elementSelector, selectCount = 1, tryTimes = 1, interval = 0) => { return new Promise(async (resolve, reject)=> { let t = 1, result; while(true) { if(selectCount != 1) {if((result = document.querySelectorAll(elementSelector)).length >= selectCount) break;} else {if(result = document.querySelector(elementSelector)) break;} if(tryTimes>0 && ++t>tryTimes) return reject(new Error("Wait Timeout")); await delay(interval); } resolve(result); }) } const id = GM_getValue("id", {}) const songID = document.URL.split("/").pop() // ===== 更改片源 ====== if (songID in id) document.querySelectorAll("[data-video-id]").forEach((e)=>e.setAttribute("data-video-id", id[songID])) const oldLink = document.createElement("div") const linkArea = document.querySelector(".alert") oldLink.innerHTML = linkArea.innerHTML oldLink.style.width = "max-content" linkArea.innerHTML="" linkArea.append(oldLink) if (songID in id) { oldLink.style["text-decoration"] = "line-through" const newLink = document.createElement("div") newLink.innerHTML = `替換影片:<a href="https://youtu.be/${id[songID]}" target="_blank">https://youtu.be/${id[songID]}</a>` linkArea.append(newLink) } const button = document.createElement("div") button.style = "position: absolute;top: 0;width: 100%;height: 100%;display: flex;align-items: center;margin-left: 8px" button.innerHTML = `<button type="button" class="btn btn-dark ml-4"><i class="bi bi-arrow-repeat"></i>更換影片</button>` new ResizeObserver(() => { const width = getComputedStyle(oldLink).width button.style.left = width button.style.width = `calc(100% - ${width})` }).observe(oldLink); linkArea.append(button) const origVid = oldLink.querySelector("a").href.split("/").pop() button.querySelector("button").onclick = ()=> { const res = prompt(`請輸入欲替換的 Youtube影片 的 網址 或 ID\n此歌曲原始ID為: ${origVid}`, origVid) if(!res) return; const vid = res.length==11 ? res : res.match(/(?<=\/|v=)[A-Za-z0-9_\-]{11}/) id[songID] = vid if (vid == origVid) delete id[songID] GM_setValue("id", id) location.reload() } // ===== 更改延遲 ====== const delayNum = GM_getValue("delayNum", {}) const delayDiv = document.createElement("div") delayDiv.hidden = true delayDiv.innerHTML = ` <div class="dropdown-menu delayInput"> <span class="minus x10"><<</span> <span class="minus x1"><</span> <div class="s"><input id="delayInput" type="number" value="${delayNum[songID] || 0}" step="0.1"></div> <span class="plus x1">></span> <span class="plus x10">>></span> </div> <style> .delayInput * {box-sizing: border-box;} .delayInput { display:block; overflow:unset; min-width: unset; width: max-content; left: -90% !important; top: 100% !important; position: absolute; z-index: 114514; transform: unset !important; } .delayInput input { font-size: 1rem; height: 34px; background-color: #fff; border: none; float: left; width: 60px; line-height: 32px; text-align: center; font-family: "helveticaneuecyrbold"; padding: 0; } .delayInput input::-webkit-outer-spin-button, .delayInput input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } .delayInput .s {display:inline} .delayInput .s::after { content: "s"; font-size: 0.8rem; float: left; text-align: center; line-height: 37px; color: #888; background-color: #fff; right: 34%; position: absolute; height: 0; } .delayInput span { line-height: 33px; font-size: 16px; font-weight: bolder; letter-spacing: -5px; text-align: center; display: block; width: 32px; float: left; height: 34px; cursor: pointer; transition: all 0.3s; padding-right: 4px; } .delayInput span:hover { background-color: #d5d5d5; } </style> ` const delayInput = delayDiv.querySelector("#delayInput") for(const np of ["minus", "plus"]) { const npNum = np=="plus" ? 1 : -1 for(const multiple of ["x1", "x10"]) { const multipleNum = multiple=="x10" ? 1 : 0.1 delayDiv.querySelector(`span.${np}.${multiple}`).onclick = () => { delayInput.value = (Number(delayInput.value) + npNum*multipleNum).toFixed(1) delayInput.oninput() } } } const timeStore = [] delayInput.oninput = () => { const value = Number(delayInput.value) if (Number.isInteger(value)) delayInput.value = String(value) if (songID in id) { $player.lyrics.forEach((e, i)=> { e.st = timeStore[i].st-value e.et = timeStore[i].et-value }) } else { $player.lyricsEarlyTime = value } $player.stopLyrics() $player.playLyrics() const t = GM_getValue("delayNum", {}) t[songID] = value GM_setValue("delayNum", t) } (async ()=> { const timeBtns = [...await waitElementLoad("button.dropdown-toggle[data-original-title=歌詞提早設定]", 2, 0, 100)] let lastBtn = null; timeBtns.forEach((btn)=>btn.addEventListener("click", ()=> { btn.parentElement.append(delayDiv) delayDiv.hidden = !delayDiv.hidden if (lastBtn == btn) return delayDiv.hidden = false lastBtn = btn })) document.addEventListener("click", (e) => { if (!(delayDiv.contains(e.target) || timeBtns.some((btn)=>btn.contains(e.target)))) { delayDiv.hidden = true } }); document.querySelectorAll("button.dropdown-toggle[data-original-title=歌詞提早設定] ~ .dropdown-menu").forEach((e)=>e.remove()); while(typeof $player == 'undefined') await delay(100) while($player?.lyrics?.length === 0) await delay(100); $player.lyrics.forEach((e, i)=>{timeStore[i] = {st: e.st, et: e.et}}) delayInput.oninput() })()