替換 marumaru 影片與歌詞延遲

替換 marumaru 播放的影片成你指定的 youtube 影片,方便用純音樂版練歌,也可以調整歌詞延遲

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name         替換 marumaru 影片與歌詞延遲
// @name:en      substitute marumaru video, and offsets for lyrics
// @name:zh-TW   替換 marumaru 影片與歌詞延遲

// @description          替換 marumaru 播放的影片成你指定的 youtube 影片,方便用純音樂版練歌,也可以調整歌詞延遲
// @description:en       substitute marumaru video to your desire youtube video, also adjust offset for lyrics
// @description:zh-TW    替換 marumaru 播放的影片成你指定的 youtube 影片,方便用純音樂版練歌,也可以調整歌詞延遲

// @license MIT
// @namespace    https://greasyfork.org/users/1303696
// @version      1.1
// @author       xswzaq44321
// @match        https://www.jpmarumaru.com/tw/JPSongPlay*
// @run-at       document-end
// @icon         https://www.google.com/s2/favicons?sz=64&domain=jpmarumaru.com
// @grant        GM.getValue
// @grant        GM.setValue
// ==/UserScript==

function fromHTML(html, trim = true) {
    // Process the HTML string.
    html = trim ? html.trim() : html;
    if (!html) return null;

    // Then set up a new template element.
    const template = document.createElement('template');
    template.innerHTML = html;
    const result = template.content.children;

    // Then return either an HTMLElement or HTMLCollection,
    // based on whether the input HTML had one or more roots.
    if (result.length === 1) return result[0];
    return result;
  }

  (async function() {
      'use strict';
      if(!document.querySelector("#VideoID").textContent){ // probabliy no youtube player available, abort execution
          return;
      }
      const songPK = document.querySelector("#SongPK").textContent
      let state = await GM.getValue(songPK, {"instrumental": false, "VideoID": "", "offset": 0});
      let messages = {
          "btn": Intl.DateTimeFormat().resolvedOptions().locale == 'zh-TW' ? "切換影片" : "switch video",
          "regFail": Intl.DateTimeFormat().resolvedOptions().locale == 'zh-TW' ? "無法解析替代影片 ID" : "cannot resole video ID",
          "vidIDEmpty": Intl.DateTimeFormat().resolvedOptions().locale == 'zh-TW' ? "沒有可替換的影片 ID" : "no available video ID to substitute",
          "resetOffset": Intl.DateTimeFormat().resolvedOptions().locale == 'zh-TW' ? "重設歌詞延遲 (sec)" : "reset offset for lyrics (sec)",
      }
      var switchBtn = fromHTML(`<a href="javascript:;" class="easyui-linkbutton l-btn l-btn-small" style="margin-bottom:4px;" group="" id=""><span class="l-btn-left"><span class="l-btn-text">${state.instrumental ? "🎼" : "🎤"}${messages.btn}</span></span></a>`);
      var VideoIDInput = fromHTML(`<input style="margin:0 3px 0 3px; width: 10em" type="text" placeholder="${state.VideoID}">`);
      var resetOffsetBtn = fromHTML(`<a href="javascript:;" class="easyui-linkbutton l-btn l-btn-small" style="margin-bottom:4px;" group="" id=""><span class="l-btn-left"><span class="l-btn-text">${messages.resetOffset}</span></span></a>`);
      var offsetInput = fromHTML(`<input type="number" id="LSToffset" name="LSToffset" value="0" step="0.1" style="width: 5em; margin-left:3px">`);
      var elementWrapper = fromHTML(`<div style="padding:0 0 5px 5px"></div>`);
      elementWrapper.appendChild(switchBtn);
      elementWrapper.appendChild(VideoIDInput);
      if(state.instrumental){
          elementWrapper.appendChild(resetOffsetBtn);
          elementWrapper.appendChild(offsetInput);
      }
      const reg = new RegExp(/(?:https?:\/\/(?:www\.)?youtube\.com\/watch\?v=|https?:\/\/youtu\.be\/|^)([A-Za-z0-9_-]{11})(?:&.*)?$/)
      switchBtn.onclick = function(){
          let VideoID = state.VideoID;
          if(VideoIDInput.value){
              let regRes = reg.exec(VideoIDInput.value);
              if(!regRes){
                  alert(`${messages.regFail}`);
                  return;
              }
              VideoID = regRes[1];
          }
          if(!VideoID){
              alert(`${messages.vidIDEmpty}`);
              return;
          }
          state.instrumental = !state.instrumental;
          state.VideoID = VideoID;
          GM.setValue(songPK, state);
          location.reload();
      };
      resetOffsetBtn.onclick = function(){
          unsafeWindow.LST = offsetInput.value = 0;
          unsafeWindow.currentLyries = -1;
          if(state.instrumental){
              state.offset = 0;
              GM.setValue(songPK, state);
          }
      }
      offsetInput.oninput = function(){
          unsafeWindow.LST = parseFloat(this.value);
          unsafeWindow.currentLyries = -1;
      }
      offsetInput.onchange = function(){
          if(state.instrumental){
              state.offset = parseFloat(this.value);
              GM.setValue(songPK, state);
          }
      }
      document.querySelector("#left_col > div:nth-child(2) > div.main > div.main-content.cf > div.clear").nextElementSibling.after(elementWrapper);
      if(state.instrumental && state.VideoID){
          document.querySelector("#VideoID").textContent = state.VideoID
          offsetInput.value = state.offset;
          offsetInput.oninput();
      }
  })();