YouTube検索結果「全てキューに入れて再生」ボタンを追加

musictonicの代わり 右クリックだとシャッフル再生 e:カーソル下の動画をキューに入れる y:再生開始 Alt+c:視聴中の再生リストをURLにしてコピー

Od 27.02.2022.. Pogledajte najnovija verzija.

// ==UserScript==
// @name YouTube検索結果「全てキューに入れて再生」ボタンを追加
// @description musictonicの代わり 右クリックだとシャッフル再生 e:カーソル下の動画をキューに入れる y:再生開始 Alt+c:視聴中の再生リストをURLにしてコピー
// @version      0.1.17
// @run-at document-idle
// @match *://www.youtube.com/*
// @match *://www.youtube.com/
// @require https://code.jquery.com/jquery-3.4.1.min.js
// @require https://code.jquery.com/ui/1.12.1/jquery-ui.min.js
// @grant GM.setClipboard
// @namespace https://greasyfork.org/users/181558
// ==/UserScript==

(function() {
  const CLOSE_MINI_PLAYER_ALWAYS = 1; // 1:Escでミニプレイヤーを常に閉じる
  const AGREE_TO_CONTINUE_ALWAYS = 1; // 1:無操作一時停止を常に解除
  const HIDE_SUGGEST = 1000; // 1-:検索結果に割り込む「あなたへのおすすめ」「他の人はこちらも視聴しています」「家にいながら学ぶ」を隠す
  const YOUTUBE_WATCH_ALTC_VARIATIONS = 3; // Alt+Cの機能を何番目まで使うか 1:通常のみ 2:動画IDの列挙 3:動画URLの列挙
  const COE = 1; // chrome以外のウエイト係数 取りこぼす時は大きく
  const COE_CHROME = 1; // chromeのウエイト係数 取りこぼす時は大きく

  const CHROME = (window.navigator.userAgent.toLowerCase().indexOf('chrome') != -1);
  const WAIT_FIRST = CHROME ? 700 : 200; // 取りこぼす時は大きく
  const WAIT_MIN = CHROME ? 190 : 160; // 取りこぼす時は大きく 50-
  const WAIT_MAX = 300; // 取りこぼす時は大きく 250-
  const waitLast = performance.now() * 1; // 現在の負荷
  const wait = Math.round((Math.min(WAIT_MAX, Math.max(WAIT_MIN, waitLast / 10))) * (CHROME ? COE_CHROME : COE));
  const DEBUG = 0; // 1:wait値を表示

  String.prototype.match0 = function(re) { let tmp = this.match(re); if (!tmp) { return null } else if (tmp.length > 1) { return tmp[1] } else return tmp[0] } // gフラグ不可
  let inYOUTUBE = location.hostname.match0(/^www\.youtube\.com|^youtu\.be/);

  var videoDisplayedLast = 0;
  var lastLength = 0;
  var mllID = 0;
  var kaisuu = 0;

  var playAllCount;
  var myqueue = [];
  
  //URLの変化を監視
  var href = location.href;
  var observer = new MutationObserver(function(mutations) {
    if (href !== location.href) {
      href = location.href;
      $('#playAllButton').remove();
      setTimeout(() => {
        lastLength = 0;
        run()
      }, 1500);
    }
  });
  observer.observe(document, { childList: true, subtree: true });
  setTimeout(() => { run(); }, 1009);
  setInterval(() => { hideSuggest() }, 1511);
  if (AGREE_TO_CONTINUE_ALWAYS) {
    setInterval(() => {
      if (eleget0('//yt-formatted-string[text()="動画が一時停止されました。続きを視聴しますか?"]')) {
        elegeta('//yt-formatted-string[@class="style-scope yt-button-renderer style-blue-text size-default" and text()="はい"]').forEach(e => e.click());
      }
    }, 3001);
  }

  var mousex = 0;
  var mousey = 0;
  document.addEventListener("mousemove", function(e) {
    mousex = e.clientX;
    mousey = e.clientY;
  }, false);

  if (CLOSE_MINI_PLAYER_ALWAYS) setInterval(() => { // ミニプレイヤーを常に閉じる
    let e = eleget0('//yt-formatted-string[@id="text" and @class="style-scope yt-button-renderer style-blue-text size-default" and text()="プレーヤーを閉じる"]');
    if (e) e.click();
  }, 701);


  document.addEventListener('keydown', e => {
    if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.getAttribute('contenteditable') === 'true' || (inYOUTUBE && (document.activeElement.closest('#chat-messages') || document.activeElement.closest('ytd-comments-header-renderer')))) return;
    var key = (e.shiftKey ? "Shift+" : "") + (e.altKey ? "Alt+" : "") + (e.ctrlKey ? "Ctrl+" : "") + e.key;
    if (key === "e") { // e::enqueue
      e.preventDefault();
      var ele = document.elementFromPoint(mousex, mousey);
      var ancestorEle = getTitleFromParent(ele, 0, '//ytd-item-section-renderer|//ytd-playlist-video-renderer|//ytd-grid-video-renderer|//div[@id="dismissible" and @class="style-scope ytd-video-renderer"]|//div[@id="dismissible" and @class="style-scope ytd-rich-grid-media"]|//ytd-compact-video-renderer');
      if (!ancestorEle) return false
      let menuButton = elegeta('//yt-icon[@class="style-scope ytd-menu-renderer"]', ancestorEle);
      if (menuButton.length == 1) {
        setTimeout(() => { ancestorEle.style.opacity = 0.5 }, 0)
        setTimeout(() => { ancestorEle.style.opacity = 0.5 }, 17 * 2)
        setTimeout(() => { ancestorEle.style.opacity = 1 }, 17 * 4)
        setTimeout(() => { menuButton[0].click() }, 0);
        setTimeout(() => {
          let queue = eleget0('//yt-formatted-string[text()="キューに追加"]|//yt-formatted-string[text()="Add to queue"]');
          if (queue) queue.click();
        }, 100)
      }
      return false;
    }

    if (key === "y" && !/\/watch/.test(location.href)) { // y::start playing
      e.preventDefault();
      cli('//div[contains(@class,\"ytp-miniplayer-play-button-container\")]/button[@aria-label=\"再生(k)\"]|//button[@class="ytp-play-button ytp-button" and @aria-label="Play (k)"]')
      if (!(location.href.match(/\/watch\?v=/))) cli('//div[@class="ytp-miniplayer-scrim"]/button[@aria-label="拡大(i)"]|//div[@class="ytp-miniplayer-scrim"]/button[@aria-label="Expand (i)"]', 111, "infinity");
      setTimeout(() => { let e = eleget0('//video'); if (e) { e.play(); } }, 222);
      return false;
    }
    if (key === "Alt+c" && /\/watch/.test(location.href)) { // Alt+c::視聴中の再生リストをURLにしてコピー
      e.preventDefault();
      let eles = elegeta('//ytd-playlist-panel-video-renderer[@id="playlist-items"]/a');
      let eles2 = [...new Set(eles.map(c => c.href.match0(/\?v=([^&]+)/)))].slice(0, 50); // 重複削除
      if (eles.length) {
        let indexEle = eleget0('//yt-formatted-string[@class="index-message style-scope ytd-playlist-panel-renderer"]/span[1]|//div/div[@id="secondary-inner" and @class="style-scope ytd-watch-flexy"]/ytd-playlist-panel-renderer[@id="playlist" and @class="style-scope ytd-watch-flexy" and @js-panel-height="" and @collapsible="" and @playlist-type="TLPQ"]/div/div[1]/div[@id="header-contents"]/div[@id="header-top-row" and contains(@class,"style-scope ytd-playlist-panel-renderer")]/div[@id="header-description"]/div/div/span');
        let indexNo = indexEle && indexEle.textContent ? indexEle.textContent.match0(/(\d+)/mi) - 1 : 0;
        let indexUrlQP = indexNo > 0 ? `&index=${indexNo}` : "";
        elegeta("#link4bm").forEach(e => e.remove())
        if (kaisuu == 2) {
          list = [...new Set(elegeta('//h4[@class=\"style-scope ytd-playlist-panel-video-renderer\"]/span[@id=\"video-title\"]').map(e => { return e.textContent.trim() + "\n" + e.closest('a').href.trim().replace(/&.*/, "") + "\n" }).map(a => JSON.stringify(a)))].map(a => JSON.parse(a)).join("")
          popup(list, "#303060")
          GM.setClipboard(list + "");
        } else if (kaisuu == 1) {
          let list = "," + eles2.join(",")
          popup(list)
          GM.setClipboard(list + "\n");
        } else {
          var cb = kaisuu == 2 ? `https://www.youtube.com/embed/${eles2[0]}?playlist=${ eles2.join(",")}` : "https://www.youtube.com/watch_videos?video_ids=" + eles2.join(",") + indexUrlQP;
          var embedHTML = `<iframe referrerpolicy="no-referrer" src="${cb}" id="ytplayer" type="text/html" width=320 height=180 frameborder=0 allowfullscreen>`
          var cb2 = cb
          var cbEsc = (cb2).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;')
          var title = `▶ ${indexNo+1}/${eles2.length} ${document.title}`
          popup(document.title + "\n" + cb)

          GM.setClipboard(document.title + "\n" + cb2 + "\n");
          $(`<div style="font-size:14px;" id="link4bm">${kaisuu==2?"埋め込み":"ブックマーク"}用リンク(${eles2.length})<br><a href=${cb}>${title}</a></div>`).hide(0).insertBefore($(elegeta('//div/ytd-playlist-panel-renderer'))).show(150).delay(9999).hide(250, function() { $(this).remove() })

          let eley = 0; //$(".cccbox").offset().top - $(window).scrollTop() + $(quoteDesEle).outerHeight() + 1;
          let marginheight = $('#link4bm').offset().top; //Math.min(window.innerHeight, document.documentElement.clientHeight) - eley
          let eleheight = $("#cccbox").height() + 4
          if (marginheight / eleheight < 1) $("#cccbox").css({ "transform-origin": "top right", "transform": `scale(${Math.max(0.2,marginheight/eleheight)})` })

        }
        kaisuu = ++kaisuu % YOUTUBE_WATCH_ALTC_VARIATIONS;
        return false
      }
    }
  }, false)

  return;

  function hideSuggest() {
    if (HIDE_SUGGEST && location.href.indexOf('www.youtube.com/results?') !== -1) {
      ['//div/div/span[@id="title"  and (text()="Learn while you\'re at home" or text()="For you" or text()="People also watched" or text()="家にいながら学ぶ" or text()="あなたへのおすすめ" or text()="他の人はこちらも視聴しています")]/../../../../..', // 縦横
        '//div[@class="style-scope ytd-shelf-renderer"]/h2[@class="style-scope ytd-shelf-renderer"]/span[@id="title" and contains(@class,"style-scope ytd-shelf-renderer") and (text()="Learn while you\'re at home" or text()="For you" or text()="People also watched" or text()="家にいながら学ぶ" or text()="あなたへのおすすめ" or text()="他の人はこちらも視聴しています")]/../../../..', // 縦1列
      ].forEach(xp => {
        $(elegeta(xp)).hide(HIDE_SUGGEST, function() { $(this).remove() }); // 検索結果に割り込むサジェストを隠す
      });
    }
  }

  function run(node = document) {
    if (location.href == "https://www.youtube.com/" ||
      location.href.match(/https:\/\/www\.youtube\.com\/results\?.*(q=|search_query=)/) ||
      location.href.match("//www.youtube.com/channel/.*/search|//www.youtube.com/user/.*/search") ||
      (location.href.match("//www.youtube.com/channel/|//www.youtube.com/c/|//www.youtube.com/user/") && !(location.href.match("/community|/channels|/about|/playlists"))) ||
      location.href.match("//www.youtube.com/playlist") ||
      location.href.match("//www.youtube.com/watch")) {
      var place = eleget0('//div[@id="center" and @class="style-scope ytd-masthead"]');
    } else return;

    if (place) {
      $('#playAllButton').remove();
      var playAllButton = $('<span class="ignoreMe" style="cursor:pointer;color:var(--yt-spec-icon-active-other); text-align:center; font-size:15px; " title="クリックで画面に出ている動画を全てキューに入れて再生(右クリックだとシャッフル)\nEnqueue all displayed videos and start playing (right-click to shuffle)" id="playAllButton">Play All</span>')
      playAllButton.insertAfter(place);
      playAllButton.on("contextmenu", () => { playAll("shuffle"); return false; });
      playAllButton.on("click", () => { playAll(); return false; });
      if (!playAllCount) {
        playAllCount = setInterval(() => {
          let currentLength = elegeta('//yt-icon[@class="style-scope ytd-menu-renderer"]').length;
          if (lastLength != currentLength) $('#playAllButton').html("Play All (" + currentLength + ")" + (DEBUG ? "<br>wait:" + wait : ""));
          lastLength = currentLength;
        }, 1000);
      }
    }

  }

  function pauseVideo() {
    let e = eleget0('//video');
    if (e) { e.pause(); } else { setTimeout(pauseVideo, 17) }
  }

  function playAll(option = false) {
    setTimeout(pauseVideo, 17);
    let videoLength = elegeta('//yt-icon[@class="style-scope ytd-menu-renderer"]', document, 0).length;
    //notifyMe(videoLength * 2)
    elegeta('//ytd-rich-item-renderer|//div[@id="dismissible"]', document.body, 0).forEach(e => { e.remove(); });
    let d = 0;
    let videoEle = elegeta('//yt-icon[@class="style-scope ytd-menu-renderer"]');
    for (let e of (option == "shuffle" ? shuffle(videoEle) : videoEle)) {
      setTimeout(() => { e.click() }, d);
      if (d == 0) d += WAIT_FIRST + (videoLength * 2); // ?
      setTimeout(() => {
        let queue = eleget0('//yt-formatted-string[text()="キューに追加"]|//yt-formatted-string[text()="Add to queue"]');
        if (queue) queue.click();
      }, d + wait / 2);
      d += wait + (videoLength / 5);
    }
    d += wait * Math.min(7000, Math.max(2000, waitLast)) / 1000 + videoLength / 3;
    cli('//div[contains(@class,\"ytp-miniplayer-play-button-container\")]/button[@aria-label=\"再生(k)\"]|//button[@class="ytp-play-button ytp-button" and @aria-label="Play (k)"]', d);
    d += wait * Math.min(7000, Math.max(2000, waitLast)) / 1000 + videoLength / 3;
    if (!(location.href.match(/\/watch\?v=/))) cli('//div[@class="ytp-miniplayer-scrim"]/button[@aria-label="拡大(i)"]|//div[@class="ytp-miniplayer-scrim"]/button[@aria-label="Expand (i)"]', d, "infinity");
    d += wait;
    setTimeout(() => { let e = eleget0('//video'); if (e) { e.play(); } }, d);
  }

  function shuffle(array) {
    for (let i = array.length - 1; i >= 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [array[i], array[j]] = [array[j], array[i]];
    }
    return array;
  }

  function cli(xpath, wait, mode = "") { // mode: infinity:押せるまで監視し続ける
    setTimeout(() => {
      let ele = eleget0(xpath);
      if (ele) { ele.click(); } else if (mode === "infinity") { cli(xpath, 200, mode) }
    }, wait);
    if (eleget0(xpath)) { return true } else { return false }
  }

  function elegeta(xpath, node = document, onlyVisible = 1) {
    if (!xpath) return [];
    if (!/^\.?\/\//.test(xpath)) return document.querySelectorAll(xpath);
    try {
      var array = [];
      var ele = document.evaluate("." + xpath, node, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
      let j = 0;
      for (var i = 0; i < ele.snapshotLength; i++) {
        let ei = ele.snapshotItem(i);
        if (ei.offsetHeight) { if (onlyVisible) { array[j++] = ei; } } else { if (!onlyVisible) { array[j++] = ei; } }
      }
      return array;
    } catch (e) { return []; }
  }

  function eleget0(xpath, node = document) {
    if (!xpath) return null;
    try {
      var ele = document.evaluate(xpath, node, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
      if (ele.snapshotLength < 1) return "";
      let ei = ele.snapshotItem(0);
      if (ei.offsetHeight) return ei;
      return "";
    } catch (e) { return null; }
  }

  function notifyMe(body, title = "") {
    if (!("Notification" in window)) return;
    else if (Notification.permission == "granted") new Notification(title, { body: body });
    else if (Notification.permission !== "denied") Notification.requestPermission().then(function(permission) {
      if (permission === "granted") new Notification(title, { body: body });
    });
  }

  function getTitleFromParent(ele, nodisplay = 0, ancestorXP) { // ele要素の親の出品物タイトルを返す
    if (elegeta(ancestorXP).includes(ele)) return ele;
    for (let i = 0; i < (9); i++) {
      var ele2 = elegeta(ancestorXP, ele);
      if (ele2.length === 1) {
        return ele2[0];
      }
      if (ele === document) return;
      ele = ele.parentNode;
      if (elegeta(ancestorXP).includes(ele)) return ele
    }
    return;
  }

  function popup(text, bgcolor = "") {
    var e = document.getElementById("cccbox");
    if (e) { e.remove(); }
    if (mllID) { clearTimeout(mllID); }
    if (text > "" == false) return;
    text = text.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/`/g, '&#x60;').replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\n/gm, "<br>")
    var ele = document.body.appendChild(document.createElement("span"));
    bgcolor = bgcolor || (/www\.translatetheweb\.com|\.translate\.goog\/|translate\.google\.com|\/embed\//gmi.test(location.href + " " + text) ? "#822" : "#6080ff");
    ele.innerHTML = `<span id="cccbox" style="all:initial; position: fixed; right:0em; top:0em; z-index:2147483647; opacity:1; word-break:break-all; font-size:${Math.max(11,15-(text.length/300)-((text.match(/<br>/gm)||[]).length/50))}px; font-weight:bold; margin:0px 1px; text-decoration:none !important; text-align:none; padding:1px 6px 1px 6px; border-radius:12px; background-color:${bgcolor}; color:white; ">${ text }</span>`;
    mllID = setTimeout(function() { var ele = document.getElementById("cccbox"); if (ele) ele.remove(); }, 4000);
    ele.onclick = () => { var e = document.getElementById("cccbox"); if (e) { e.remove(); } if (mllID) { clearTimeout(mllID); } }
  }

})();