Greasy Fork is available in English.

jpmarumaru 更改片源與播放延遲

更換翻唱歌曲、更精確校時

// ==UserScript==
// @name           jpmarumaru 更改片源與播放延遲
// @namespace      Anong0u0
// @version        0.2.2
// @description    更換翻唱歌曲、更精確校時
// @author         Anong0u0
// @match          https://www.jpmarumaru.com/tw/*
// @icon           https://www.google.com/s2/favicons?sz=64&domain=jpmarumaru.com
// @grant          GM_setValue
// @grant          GM_getValue
// @grant          unsafeWindow
// @license        Beerware
// ==/UserScript==

const delay = (ms = 0) => {return 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.querySelector("#SongPK").textContent
const ytid = document.querySelector("#VideoID").textContent

// ===== 更改片源 ======

if (songID in id) waitElementLoad("iframe#divVideo", 1, 0, 10).then((vid)=>{vid.src = vid.src.replace(ytid, id[songID])})

const inp = document.createElement("input")
const btn = document.createElement("button")

btn.innerText = "確定"
btn.style = "font-size: 12px"
btn.hidden = true
btn.onclick = () =>
{
    const t = GM_getValue("id", {})
    switch(btn.innerText)
    {
        case "更改":
            t[songID] = inp.value
            break;
        case "復原":
            t[songID] = ytid
            break;
    }
    GM_setValue("id", t)
    location.reload()
}

inp.style = "margin-left: 16px;height: 16px;"
inp.maxLength = 11
inp.oninput = () =>
{
    const value = inp.value
    if(value.length == 11 && ((value != ytid && value != id[songID]) || (value == ytid && id[songID] != ytid)))
    {
        btn.hidden = false
        btn.innerText = "更改"
    }
    else btn.hidden = true
}

const head = document.querySelector("#header_meta")
head.onmouseenter = () =>
{
    inp.style.width = "9em"
    inp.value = id[songID] || ytid
    if(inp.value != ytid)
    {
        btn.hidden = false
        btn.innerText = "復原"
    }
}
head.onmouseleave = () =>
{
    inp.style.width = "3em"
    inp.value = id[songID] && id[songID] != ytid ? "已替換" : "未變更"
    btn.hidden = true
}
head.onmouseleave()

const ul = document.querySelector("#bread_crumb")
ul.append(inp)
ul.append(btn)


// ===== 更改延遲 ======

const delayNum = GM_getValue("delayNum", {})
const div = document.createElement("div")
div.hidden = true
div.innerHTML = `
<div class="dropdown-content 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: -30px;
}

.delayInput input {
	font-size: 14px;
	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";
    float: left;
    text-align: center;
    line-height: 37px;
    background-color: #fff;
    right: 69px;
    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>
`
document.querySelector(":last-child > .dropdown").append(div)

const delayInput = document.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
        document.querySelector(`span.${np}.${multiple}`).onclick = () =>
        {
            delayInput.value = (Number(delayInput.value) + npNum*multipleNum).toFixed(1)
            delayInput.onchange()
        }
    }
}

Number.prototype.getTimeSpan = function()
{
    const hours = String(Math.floor(this / 3600000)).padStart(2, '0');
    const minutes = String(Math.floor((this % 3600000) / 60000)).padStart(2, '0');
    const seconds = String(Math.floor((this % 60000) / 1000)).padStart(2, '0');
    const ms = String((this % 1000)).padStart(3, '0');
    return `${hours}:${minutes}:${seconds}:${ms}`;
}
const originStartTime = json.StartTime,
      originEndTime = json.EndTime

delayInput.onchange = () =>
{
    const t = GM_getValue("delayNum", {})
    let value = Number(delayInput.value)
    if (Number.isInteger(value)) {
        delayInput.value = String(value)
    }
    if(value > 0)
    {
        json.StartTime = originStartTime.map((e)=>(new Date(`1970-1-1 ${e}+0`).getTime()+value*1000).getTimeSpan())
        json.EndTime = originEndTime.map((e)=>(new Date(`1970-1-1 ${e}+0`).getTime()+value*1000).getTimeSpan())
        unsafeWindow.LST = 0
    }
    else
    {
        json.StartTime = originStartTime
        json.EndTime = originEndTime
        unsafeWindow.LST = -value
    }
    t[songID] = value
    GM_setValue("delayNum", t)
}
delayInput.onchange()

const timeBtn = document.querySelector("div:last-child > .dropdown > .dropbtn")
timeBtn.addEventListener("click", () =>
{
    div.hidden = !div.hidden
})
document.addEventListener("click", (e) =>
{
    if (!(div.contains(e.target) || timeBtn.contains(e.target))) {
        div.hidden = true
    }
});
document.querySelector("#LST").remove()