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

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

От 07.04.2023. Виж последната версия.

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

(function() {
  const USE_IMMEDIATE_PLAYLIST = 0; // 0:機能6-8を無効にする 1:有効にし使用時に確認を表示する 2:有効にし確認しない
  const YOUTUBE_WATCH_ALTC_VARIATIONS = 2; // Alt+Cの機能を何番目まで使うか 1:連続再生URL 2:単独再生URLの列挙 3:iframe埋め込み用HTML
  const CLOSE_MINI_PLAYER_ALWAYS = 1; // 1:Escでミニプレイヤーを常に閉じる
  const AGREE_TO_CONTINUE_ALWAYS = 1; // 1:無操作一時停止を常に解除
  const HIDE_SUGGEST = 1000; // 1-:検索結果に割り込む「あなたへのおすすめ」「他の人はこちらも視聴しています」「家にいながら学ぶ」を隠す
  const DEBUG = 0; // 1:wait値を表示

  const EXPERIMENTAL_FASTMODE = 1; // 1:実験的な高速モードを使用 0:旧モード
  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 = EXPERIMENTAL_FASTMODE ? (CHROME ? 40 : 40) : Math.round((Math.min(WAIT_MAX, Math.max(WAIT_MIN, waitLast / 10))) * (CHROME ? COE_CHROME : COE));

  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フラグ不可
  function adja(place = document.body, pos, html) {
    return place ? (place.insertAdjacentHTML(pos, html), place) : null;
  }
  let inYOUTUBE = location.hostname.match0(/^www\.youtube\.com|^youtu\.be/);

  var videoDisplayedLast = 0;
  var lastLength = 0;
  var mllID = 0;
  var kaisuu = 0;
  var equeueIP = []
  var equeue = []

  var playAllCount, playAllCount2;
  var myqueue = [];

  //URLの変化を監視
  var href = location.href;
  var observer = new MutationObserver(function(mutations) {
    if (href !== location.href) {
      href = location.href;
      $('#playAllButton,#immediatePlaylistButton').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(() => {
      elegeta('tp-yt-paper-toast#toast.toast-button.yt-notification-action-renderer.paper-toast-open:visible:text*=視聴を続けていますか?動画がまもなく一時停止されます').forEach(e => eleget0('button:visible:text*=はい', e)?.click)
      if (elegeta('yt-formatted-string.line-text.style-scope.yt-confirm-dialog-renderer:visible:text*=動画が一時停止されました。続きを視聴しますか?')) { eleget0('div.cbox.yt-spec-button-shape-next--button-text-content span.yt-core-attributed-string.yt-core-attributed-string--white-space-no-wrap[role="text"]:visible:text*=はい')?.click() }
    }, 3001)
  }

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

  document.addEventListener('keydown', e => {
    if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable || ((e.target.closest('#chat-messages,ytd-comments-header-renderer') || document.activeElement?.closest('#chat-messages,ytd-comments-header-renderer')))) return;
    var key = (e.shiftKey ? "Shift+" : "") + (e.altKey ? "Alt+" : "") + (e.ctrlKey ? "Ctrl+" : "") + e.key;

    if (key === "Escape" && CLOSE_MINI_PLAYER_ALWAYS) { // esc::ミニプレイヤーを常に閉じる
      for (let i = 0; i < 20; i++) {
        setTimeout(() => { elegeta('tp-yt-paper-dialog .yt-core-attributed-string.yt-core-attributed-string--white-space-no-wrap:visible').filter(e => /プレーヤーを閉じる/.test(e.textContent)).forEach(e => e?.click()) }, i * 200)
        equeue = []
      }
    }

    if (key === "e") { // e::enqueue
      e.preventDefault();
      var ele = document.elementFromPoint(mousex, mousey);

      var box = ele.closest('ytd-video-renderer,ytd-rich-item-renderer,ytd-grid-video-renderer,ytd-playlist-video-renderer'); //マウスが乗っている動画の枠
      if (box) { //IP先頭用に独自キューを覚えておく
        var href = eleget0('//a', box)?.href;
        var vID = href?.match0(/\?v=([a-zA-Z0-9_\-]{11})/) || href?.match0(/\/shorts\/([a-zA-Z0-9_\-]{11})/);
        if (vID) {
          equeueIP.push(vID)
          equeueIP = [...new Set(equeueIP)]
          equeue.push(vID)
          equeue = [...new Set(equeue)]
          $('#immediatePlaylistButton').html(`Immediate<BR>Playlist (${equeueIP.length}+)`)
        }
      }

      var prevcue = eleget0('//ytd-thumbnail-overlay-toggle-button-renderer[@aria-label="キューに追加"]/yt-icon[2]|.//ytd-thumbnail-overlay-toggle-button-renderer[@aria-label="Add to queue"]/yt-icon[2]', box)
      if (prevcue) { prevcue?.click(); return false; }
      var prevcue = eleget0('//a[@id="thumbnail"]/div/ytd-thumbnail-overlay-toggle-button-renderer[last()]/yt-icon[@class="style-scope ytd-thumbnail-overlay-toggle-button-renderer"]', box)
      //if (prevcue) { prevcue?.click(); return false; }
      var ances = box; //ele?.closest('.ytd-grid-renderer,.ytd-compact-video-renderer')

      if (ances) {
        var cuebutton = elegeta('ytd-thumbnail.style-scope.ytd-grid-video-renderer a div ytd-thumbnail-overlay-toggle-button-renderer:last-child yt-icon#icon,ytd-thumbnail.ytd-compact-video-renderer a div ytd-thumbnail-overlay-toggle-button-renderer:last-child yt-icon#icon', ances)[0]
        if (cuebutton) {
          cuebutton?.click()
          ances.style.opacity = "0.25"
          setTimeout(() => { ances.style.opacity = "0.5" }, 17 * 2)
          setTimeout(() => { ances.style.opacity = "1" }, 17 * 3)
          return false
        }
      }

      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(() => {
          let queue = eleget0('//yt-formatted-string[text()="キューに追加"]|//yt-formatted-string[text()="Add to queue"]');
          if (queue) {
            queue.click();
            setTimeout(() => { ancestorEle.style.opacity = 0.5 }, 0)
            setTimeout(() => { ancestorEle.style.opacity = 0.5 }, 17 * 2)
            setTimeout(() => { ancestorEle.style.opacity = 1 }, 17 * 4)
          }
        }, 200)
        setTimeout(() => { menuButton[0].click() }, 0);
      }
      return false;
    }

    if (key === "y" && !/\/watch/.test(location.href)) { // y::start playing
      e.preventDefault();
      cli('//div[contains(@class,"ytp-miniplayer-play-button-container")]/button|//button[contains(@class,"ytp-play-button-playlist")]')
      if (!(location.href.match(/\/watch\?v=/))) cli('//div/button[contains(@class,"ytp-miniplayer-expand-watch-page-button")]:visible', 111, "infinity");
      setTimeout(() => { let e = eleget0('//video'); if (e) { e.play(); } }, 222);
      return false;
    }
    if (key === "Alt+c" && /\/watch/.test(location.href) && USE_IMMEDIATE_PLAYLIST) { // Alt+c::視聴中の再生リストをURLにしてコピー
      e.preventDefault();
      makeUrlFromCuelist(1, kaisuu)
      kaisuu = ++kaisuu % YOUTUBE_WATCH_ALTC_VARIATIONS;
    }
  }, false)

  return;

  function makeUrlFromCuelist(disp = 1, kaisuu) { // disp:1:表示する 0:urlを作って返すだけ
    if (/\/watch/.test(location.href) && USE_IMMEDIATE_PLAYLIST) { // Alt+c::視聴中の再生リストをURLにしてコピー
      let eles = elegeta('//ytd-playlist-panel-video-renderer[@id="playlist-items"]/a:visible');
      let videoIDa = [...new Set(eles.map(c => c.href.match0(/\?v=([a-zA-Z0-9_\-]{11})/)))].slice(0, 50); // 重複削除
      let videoIDaAll = [...new Set(eles.map(c => c.href.match0(/\?v=([a-zA-Z0-9_\-]{11})/)))]; // 重複削除
      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:visible');
        let indexNo = indexEle && indexEle.textContent ? indexEle.textContent.match0(/(\d+)/mi) - 1 : 0;
        let indexUrlQP = (indexNo > 0 && indexNo < 50) ? `&index=${indexNo}` : "";
        elegeta("#link4bm").forEach(e => e.remove())
        if (kaisuu == 0 || kaisuu == 2 || disp == 0) {
          // プレイリストの動画の合計時間を算出、全部読み込みが終わっていないとしない
          let playtimesum = (elegeta('//ytd-playlist-panel-video-renderer[@class="style-scope ytd-playlist-panel-renderer" and @watch-color-update="" and @id="playlist-items"]:visible').slice(0, 49).length != elegeta('//ytd-playlist-panel-video-renderer[@class="style-scope ytd-playlist-panel-renderer" and @watch-color-update="" and @id="playlist-items"]//a[@id="thumbnail" and @class="yt-simple-endpoint inline-block style-scope ytd-thumbnail" and @tabindex="-1" and @rel="null"]/div[@id="overlays"]/ytd-thumbnail-overlay-time-status-renderer[@overlay-style="DEFAULT"]/span[@id="text"]:visible').slice(0, 49).length) ? "" :
            (() => {
              let sum = elegeta('//a[@id="thumbnail" and @class="yt-simple-endpoint inline-block style-scope ytd-thumbnail" and @tabindex="-1" and @rel="null"]/div[@id="overlays"]/ytd-thumbnail-overlay-time-status-renderer[@overlay-style="DEFAULT"]/span[@id="text"]:visible').reduce((p, e) => {
                let t = e?.innerText?.trim()
                let h = t?.match0(/(\d+)\:\d+\:\d+$/) || 0
                let m = t?.match0(/(\d+)\:\d+$/) || 0
                let s = t?.match0(/(\d+)$/) || 0
                p += h * 60 * 60 + m * 60 + s * 1
                return p
              }, 0)
              return ` ${sum/60/60|0}:${sum/60%60|0}:${sum%60}`
            })()

          var cb = kaisuu == 2 ? `<iframe referrerpolicy="no-referrer" src="https://www.youtube.com/embed/${videoIDa[0]}?playlist=${ videoIDa.join(",")}" id="ytplayer" type="text/html" allowfullscreen="" allow="picture-in-picture" width="320" height="180" frameborder="0"></p></iframe>` : "https://www.youtube.com/watch_videos?video_ids=" + videoIDa.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 enumText = ``; // 50件ごとに分割(使わない)
          for (let u = 0; u < videoIDaAll.length / 50; u++) {
            enumText += `▶ ${u*50+1}/${videoIDaAll.length} ${videoIDaAll.slice(u*50,u*50+3).join(",")+(videoIDaAll.slice(u*50,u*50+50).length>3?",…":"")}\nhttps://www.youtube.com/watch_videos?video_ids=${ (videoIDaAll.slice(u*50, u*50+50).join(",") ) }\n`
          }

          let transratedTitle = (eleget0('//h1[@class=\"title style-scope ytd-video-primary-info-renderer\"]/yt-formatted-string/font/font|//div[@id=\"title\" and contains(@class,\"style-scope ytd-watch-metadata\")]/h1/yt-formatted-string/font/font')?.innerText?.replace(/(?!= - YouTube)$/, " - YouTube")) || document.title

          var title = `▶ ${indexNo+1}/${videoIDa.length} ${transratedTitle}`
          if (disp) {
            if (videoIDaAll.length <= 50) {
              popup(kaisuu == 2 ? cb : transratedTitle + "\n" + cb, "", "right:0em; top:0em;max-width:40%;")
              GM.setClipboard(kaisuu == 2 ? cb : transratedTitle + "\n" + cb2 + "\n");
            } else {
              popup(kaisuu == 2 ? cb : transratedTitle + "\n" + enumText, "", "right:0em; top:0em;max-width:40%;")
              GM.setClipboard(kaisuu == 2 ? cb : transratedTitle + "\n" + enumText + "\n");
            }
            if (kaisuu != 2) $(`<div style="margin-left:4em;font-size:14px;" id="link4bm">${kaisuu==2?"埋め込み":"ブックマーク"}用リンク(${videoIDa.length})${playtimesum}<br><a href=${cb}>${title}</a></div>`).hide(0).insertAfter($('#logo')).show(150).delay(9999).hide(250, function() { $(this).remove() })
          }
        } else if (kaisuu == 1) {
          list = [...new Set(elegeta('//h4[@class=\"style-scope ytd-playlist-panel-video-renderer\"]/span[@id=\"video-title\"]:visible').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("")
          if (disp) {
            popup(list, "#303060")
            GM.setClipboard(list + "");
          }
          return list
        }

      }
      //        kaisuu = ++kaisuu % YOUTUBE_WATCH_ALTC_VARIATIONS;
      return cb
    } else { //キューやプレイリスト再生状態ではない
      // urlExtractAndConcat()
      // return false
      return
    }
  }


  function urlExtractAndConcat(option = "") { // url Extract & Concat
    var cb = makeUrlFromCuelist(0)
    var inp = prompt(`${option=="shuffle"?"<シャッフル>\n\n":""}url Extract & Concat:\n複数のYouTubeの動画URLから連続再生URLを作ってクリップボードにコピーします\nYouTubeの動画URLを何行でも貼り付けてください\nYouTubeのURLになっていない行や文字列は全て読み飛ばされ、重複した動画は削除されます\n${cb?`\n今視聴中のキュー/プレイリストの動画(最大50)が初期値として入力済みです\nこれを利用して前後に追加することも、削除して新しく入力することもできます\n\n${cb}\n`:""}\n対応書式:\nhttps://www.youtube.com/watch?v=動画ID\nhttps://www.youtube.com/shorts/動画ID\nhttps://youtu.be/動画ID\nhttps://www.youtube.com/watch_videos?video_ids=動画ID,動画ID,…\n\n`, cb ? ` ${cb} ` : "")
    if (inp) {
      var urlcap = [];
      inp.split(/\s/).forEach(v => { urlcap = urlcap.concat(...[...v?.matchAll(/^(?:h?ttps?:\/\/)?www\.youtube\.com\/watch\?v=([a-zA-Z0-9_\-]{11})|^(?:h?ttps?:\/\/)?youtu\.be\/([a-zA-Z0-9_\-]{11})|^(?:h?ttps?:\/\/)?www\.youtube\.com\/shorts\/([a-zA-Z0-9_\-]{11})|^(?:h?ttps?:\/\/)?www\.youtube\.com\/watch_videos\?video_ids=([a-zA-Z0-9_\-,]{11,600})/gmi)].map(c => c.slice(1, 999))).filter(w => w) }) // 書式が混在していても登場順に収納する
      if (urlcap) {
        let urla = urlcap.join(",").split(",").filter(c => /^[a-zA-Z0-9_\-]{11}$/.test(c)); // 動画IDは11桁
        let urllen = urla.length;
        let urla2 = [...new Set(urla)]; // 重複削除
        if (option == "shuffle") urla2 = shuffle(urla2); // シャッフル
        let urllen2 = urla2.length;
        let urla3 = [...urla2].slice(0, 50); // 50件まで
        let urlenum = urla3.join(",")
        let url = `https://www.youtube.com/watch_videos?video_ids=${urlenum}`
        if (urla3 && urla3.length) {
          var title = `▶ (${urla3.length}) ${urla3.slice(0,3).join(",")+(urla3.length>3?",…":"")}\n`
          var enumText = ``
          var enumUrl = []
          for (let u = 0; u < urla2.length / 50; u++) {
            enumText += `▶ ${u*50+1}/${urla2.length} ${urla2.slice(u*50,u*50+3).join(",")+(urla2.slice(u*50,u*50+50).length>3?",…":"")}\nhttps://www.youtube.com/watch_videos?video_ids=${ (urla2.slice(u*50, u*50+50).join(",") ) }\n`
            enumUrl.push(`https://www.youtube.com/watch_videos?video_ids=${ (urla2.slice(u*50, u*50+50).join(",") ) }`)
          }
          let con = USE_IMMEDIATE_PLAYLIST == 1 ? confirm(`${urllen}件の動画IDを抽出しました\n${urllen-urllen2}件の重複を削除しました\n\n下記(${urla2.length}件)をクリップボードにコピーしますか?\n\n${enumText}`) : 1;
          if (con) {
            //$("#logo").css({ "margin-right": "4em" })
            $(`<div style="margin-left:4em;font-size:14px;" id="link4bm">${kaisuu==2?"埋め込み":"ブックマーク"}用リンク(${urla3.length})<br><a title="Shift+左クリック:以下をすべて開く(${urllen2})\n${enumUrl.join("\n")}" data-urls="${escape(JS(enumUrl))}" href=${url}>${title}</a></div>`).hide(0).insertAfter($('#logo')).show(150).delay(9999).hide(250, function() { $(this).remove() })
            $('#link4bm').on("click", e => {
              if (e.shiftKey) {
                e.preventDefault()
                e.stopPropagation()
                pauseVideo()
                var enumURLsa = JP(unescape(e.target.dataset.urls))
                enumURLsa.forEach((v, i) => { setTimeout(() => i == 0 ? window.open(v) : GM.openInTab(v, true), i * 5000) }) // Shift+左クリック
                return false
              }
            })
            GM.setClipboard(enumText)
            popup(enumText, "", "right:0em; top:0em;max-width:40%;")
          }
        }
      }
    }
  }

  function hideSuggest() {
    if (HIDE_SUGGEST && location.href.indexOf('www.youtube.com/results?') !== -1) {
      ['ytd-shelf-renderer,ytd-horizontal-card-list-renderer', // 縦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\/|c\/|user\/|@)[^\/]+\/search/) ||
      (location.href.match(/\/\/www\.youtube\.com\/(?:channel\/|c\/|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) {
      if (USE_IMMEDIATE_PLAYLIST) {
        $('#immediatePlaylistButton,#extractAndConcatButton').remove();

        $('ytd-topbar-logo-renderer#logo:not([data-rcli])').on("contextmenu", () => { urlExtractAndConcat(); return false; }); // ytアイコン右クリック
        var lmd = []; // ytアイコン右長押し
        var e = eleget0('ytd-topbar-logo-renderer#logo:not([data-rcli])')
        if (e) {
          e.dataset.rcli = 1;
          //e.style.boxShadow = "1px 1px 1px 0px #0004"
          e?.addEventListener("mouseup", e => e?.button && clearTimeout(lmd[e?.button]))
          e?.addEventListener("mousedown", e => {
            e.preventDefault();
            lmd[e.button] = setTimeout(e => {
              if (e.button == 2) { urlExtractAndConcat("shuffle"); return false; }
            }, 300, e)
          })
        }

        var immediatePlaylistButton = $(`<span class="ignoreMe" style="cursor:pointer;color:var(--yt-spec-icon-active-other); text-align:center; font-size:15px; " title="クリックで画面に出ている動画を限定公開プレイリストにして再生(右クリックだとシャッフル)\nGenerate playlist from all displayed videos and open immediately (right-click to shuffle)" id="immediatePlaylistButton">Immediate<br>Playlist${equeueIP.length?" ("+equeueIP.length+"+)":""}</span>`)
        immediatePlaylistButton.insertAfter(place);
        /*        immediatePlaylistButton.on("contextmenu", (e) => { playAllG(e, "shuffle"); return false; });
                immediatePlaylistButton.on("click", (e) => { playAllG(e); return false; });
          */
        immediatePlaylistButton.on("contextmenu", e => false)
        immediatePlaylistButton.on("mousedown", (e) => {
          if (e.button == 0) playAllG(e);
          if (e.button == 2) playAllG(e, "shuffle");
          return false;
        });

        immediatePlaylistButton.on("mousemove", () => {
          var [url, len, lenmax, enumURLsa] = getUrla();
          //          $('#immediatePlaylistButton').attr("title", `クリックで画面に出ている動画を限定公開プレイリストにして再生 (${len}/${lenmax})\n(右クリックだとシャッフル、Ctrl+で新しいタブで開く)\nGenerate playlist from all displayed videos and open immediately (right-click to shuffle)\n\n${url}`);
          $('#immediatePlaylistButton').attr("title", `クリックで画面に出ている動画を限定公開プレイリストにして再生 (${len}/${lenmax})\n(右クリックだとシャッフル、Ctrl+で新しいタブで開く、Shift+で51件以上も分割して開く)\nGenerate playlist from all displayed videos and open immediately (right-click to shuffle)\n\n${enumURLsa?.join("\n\n")}`);
          return false;
        });
      }
      $('#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)\nCtrl+だと再生を始めない" id="playAllButton">Play All</span>')
      playAllButton.insertAfter(place);
      //    playAllButton.on("contextmenu", (e) => { playAll("shuffle", e); return false; });
      playAllButton.on("contextmenu", (e) => { e.preventDefault(); return false; });
      //      playAllButton.on("click", (e) => { playAll("", e); return false; });
      playAllButton.on("mousedown", (e) => { playAll(e?.button != 0 ? "shuffle" : "", e); return false; });
      if (!playAllCount) {
        playAllCount = setInterval(() => {
          //          let currentLength = elegeta('//yt-icon[@class="style-scope ytd-menu-renderer"]').length;
          let currentLength = elegeta('yt-icon.style-scope.ytd-menu-renderer:not(ytd-reel-item-renderer .ytd-menu-renderer):visible').length;
          if (lastLength != currentLength) $('#playAllButton').html("Play All (" + currentLength + ")" + (DEBUG ? "<br>wait:" + wait : ""));
          lastLength = currentLength;
        }, 1000);
      }
    }
  }

  function playAllG(e, option = false) {
    setTimeout(pauseVideo, 17);
    var [url, len, lenmax, enumURLsa] = getUrla(option)
    if (len > 0)
      if (USE_IMMEDIATE_PLAYLIST == 2 || (USE_IMMEDIATE_PLAYLIST == 1 && confirm(`下記を開きます。よろしいですか?` + (e.shiftKey ? `(${lenmax})\n\n${enumURLsa.join("\n\n")}` : `(${len})\n\n${url}`)))) {
        if (e.shiftKey) {
          enumURLsa.forEach((v, i) => { setTimeout(() => i == 0 ? window.open(v) : GM.openInTab(v, true), i * 5000) }) // Shift+左クリック
        } else
        if (e.ctrlKey) { window.open(url) } else { location.href = url }
      }
  }

  function getUrla(option) {
    let videoEle = elegeta('#dismissible a#video-title[href*="/watch?v="],#dismissible a#video-title-link[href*="/watch?v="],.style-scope.ytd-playlist-video-list-renderer a#video-title[href*="/watch?v="],#dismissible a.ytd-compact-video-renderer[href*="/watch?v="],#playlist-items a[href*="/watch?v="],#dismissible a#video-title[href*="/shorts/"],#dismissible a#video-title-link[href*="/shorts/"],.style-scope.ytd-playlist-video-list-renderer a#video-title[href*="/shorts/"],#dismissible a.ytd-compact-video-renderer[href*="/shorts/"],#playlist-items a[href*="/shorts/"],ytd-rich-grid-slim-media[mini-mode][is-short] div div a:visible');
    if (videoEle.length) {
      var videoIDa = [...new Set(videoEle.map(v => v.href.match0(/\/watch\?v=([a-zA-Z0-9_\-]{11})/) || v.href.match0(/\/shorts\/([a-zA-Z0-9_\-]{11})/)).filter(v => v))]
      /*var videoIDaMax = [...new Set([...equeueIP, ...videoIDa])]
      videoIDaMax = ((option === "shuffle") ? shuffle(videoIDaMax) : videoIDaMax)*/
      var videoIDaMax = [...new Set([...equeueIP, ...((option === "shuffle") ? shuffle(videoIDa) : videoIDa)])]
      if (videoIDa.length) {
        videoIDa = ((option === "shuffle") ? shuffle(videoIDa) : videoIDa)
        var videoIDa = [...new Set([...equeueIP, ...videoIDa])].slice(0, 50)
        let url = `https://www.youtube.com/watch_videos?video_ids=${videoIDa.join(",")}`

        var enumURLsa = []
        for (let u = 0; u < videoIDaMax.length / 50; u++) {
          enumURLsa.push(`https://www.youtube.com/watch_videos?video_ids=${ (videoIDaMax.slice(u*50, u*50+50).join(",") ) }`)
        }

        return [url, videoIDa.length, videoIDaMax.length, enumURLsa]
      }
    } else return ["", 0, 0];
  }

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

  function playAll(option = false, ev) {
    pauseVideo(); //setTimeout(pauseVideo, 17);
    var box = elegeta('ytd-video-renderer,ytd-rich-item-renderer,ytd-grid-video-renderer,ytd-playlist-video-renderer:visible').filter(v => {
      var href = eleget0('//a', v)?.href;
      var vID = href?.match0(/\?v=([a-zA-Z0-9_\-]{11})/) || href?.match0(/\/shorts\/([a-zA-Z0-9_\-]{11})/);
      return equeue.includes(vID)
    }).forEach(v => v.remove()); //マウスが乗っている動画の枠

    //elegeta('ytd-video-renderer,ytd-rich-item-renderer,ytd-grid-video-renderer,ytd-playlist-video-renderer').filter(v => !v.offsetHeight).forEach(e => e.remove())

    let d = 0;
    let videoEle = elegeta('//yt-icon[@class="style-scope ytd-menu-renderer"]:visible').filter(e => e?.closest('ytd-video-renderer,ytd-rich-item-renderer,ytd-grid-video-renderer,ytd-playlist-video-renderer')?.offsetHeight)

    let videoLength = videoEle.length
    let i = 0;
    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 + (videoLength / 20);

      if (ev.ctrlKey) { //IP先頭用に独自キューを覚えておく
        var href = eleget0('//a', e?.closest('ytd-video-renderer,ytd-rich-item-renderer,ytd-grid-video-renderer,ytd-playlist-video-renderer'))?.href
        if (href) {
          equeueIP.push(href?.match0(/\?v=([a-zA-Z0-9_\-]{11})/) || href?.match0(/\/shorts\/([a-zA-Z0-9_\-]{11})/))
          equeueIP = [...new Set(equeueIP)]
          $('#immediatePlaylistButton').html(`Immediate<BR>Playlist (${equeueIP.length}+)`)
        }
      }

      if ((i++) >= 199) break; // キューは200件までしか入らないので時間節約
    }
    d += 100 + wait * Math.min(7000, Math.max(2000, waitLast)) / 1000 + videoLength / 3;
    d = Math.max(d, CHROME ? 1500 : 800)
    if (!(location.href.match(/\/watch\?v=/))) {
      if (!ev.ctrlKey) {
        cli('//div[contains(@class,"ytp-miniplayer-play-button-container")]/button|//button[contains(@class,"ytp-play-button-playlist")]', d, "infinity", () => {
          d += 100 + wait * Math.min(7000, Math.max(2000, waitLast)) / 1000 + videoLength / 3;
          cli('//div/button[contains(@class,"ytp-miniplayer-expand-watch-page-button")]:visible', 300, "infinity")
        })
      }
    } else {
      d += 100 + 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 = "", cb = null) { // mode: infinity:押せるまで監視し続ける
    setTimeout(() => {
      let ele = eleget0(xpath);
      if (ele) { ele.click(); if (cb) cb(); } else if (mode === "infinity") { cli(xpath, 17, mode) }
    }, wait);
    if (eleget0(xpath)) { return true } else { return false }
  }

  function elegeta(xpath, node = document) {
    if (!xpath || !node) return [];
    let xpath2 = xpath.replace(/:inscreen|:visible|:text\*=[^:]*/g, "") // text*=~中で:は使えない
    let array = []
    try {
      if (!/^\.?\//.test(xpath)) {
        array = [...node.querySelectorAll(xpath2)]
      } else {
        var snap = document.evaluate("." + xpath2, node, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null)
        let l = snap.snapshotLength
        for (var i = 0; i < l; i++) array[i] = snap.snapshotItem(i)
      }
      if (/:visible/.test(xpath)) array = array.filter(e => e.offsetHeight)
      if (/:inscreen/.test(xpath)) array = array.filter(e => { var eler = e.getBoundingClientRect(); return (eler.bottom >= 0 && eler.right >= 0 && eler.left <= document.documentElement.clientWidth && eler.top <= document.documentElement.clientHeight) }) // 画面内に1ピクセルでも入っている
      if (/:text\*=./.test(xpath)) { let text = xpath.replace(/^.*:text\*=([^:]*)$/, "$1"); if (text) array = array.filter(e => new RegExp(text).test(e?.textContent)) }
    } catch (e) { alert(e); return []; }
    return array
  }

  function eleget0(xpath, node = document) {
    if (!xpath || !node) return null;
    if (/:inscreen|:visible|:text\*=/.test(xpath)) return elegeta(xpath, node)?.shift();
    if (!/^\.?\//.test(xpath)) return node.querySelector(xpath);
    try {
      var ele = document.evaluate("." + xpath, node, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
      return ele.snapshotLength > 0 ? ele.snapshotItem(0) : null;
    } catch (e) { alert(e + "\n" + xpath + "\n" + JSON.stringify(node)); 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 = "", additionalStyle = "right:0em; top:0em;") {
    text = "" + text
    var e = document.getElementById("cccboxaq");
    var cID = rndID(11);
    if (e) { e.remove(); }
    if (mllID) { clearTimeout(mllID); }
    if (!(text > "")) 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>")
    bgcolor = bgcolor || (/www\.translatetheweb\.com|\.translate\.goog\/|translate\.google\.com|\/embed\//gmi.test(location.href + " " + text) ? "#822" : "#6080ff");
    document.body.insertAdjacentHTML("beforeend", `<span id="cccboxaq" class="${cID}" style="all:initial; position: fixed;  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; ${additionalStyle}">${ text }</span>`)
    var ele = document.body.lastChild
    mllID = setTimeout((function() { return function() { $(`.${cID}`).remove(); } })(cID), 4000);
    ele.onclick = () => { $(`.${cID}`).remove(); if (mllID) { clearTimeout(mllID); } }
  }

  function rndID(n = 11) {
    var S = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"
    return Array.from(Array(n)).map(() => S[Math.floor(Math.random() * S.length)]).join('')
  }

  function ct(callback, name = "test", time = 10) { let i = 0; let st = Date.now(); while (Date.now() - st < 1000) { i++, callback() } console.log(`${name} ${i} / 1sec`) } // 速度測定
  function JS(v) { return JSON.stringify(v) }

  function JP(v) { return JSON.parse(v) }

})();