Greasy Fork

Greasy Fork is available in English.

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

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

目前为 2022-10-05 提交的版本,查看 最新版本

// ==UserScript==
// @name YouTube検索結果「全てキューに入れて再生」ボタンを追加
// @description musictonicの代わり 右クリックだとシャッフル再生 e:カーソル下の動画をキューに入れる y:再生開始 Alt+c:視聴中の再生リストをURLにしてコピー
// @version      0.1.25
// @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 http://greasyfork.icu/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 ? 17 : 2) : 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;
  }
  var JS = (v) => { return JSON.stringify(v) }
  let inYOUTUBE = location.hostname.match0(/^www\.youtube\.com|^youtu\.be/);

  var videoDisplayedLast = 0;
  var lastLength = 0;
  var mllID = 0;
  var kaisuu = 0;
  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(() => {
      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 (location.href.match0(/nicovideo/)) {
      // ニコ動
      document.addEventListener('keydown', e => {
        if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA' && e.target.getAttribute('contenteditable') != 'true') {
          var key = (e.shiftKey ? "Shift+" : "") + (e.altKey ? "Alt+" : "") + (e.ctrlKey ? "Ctrl+" : "") + e.key;
          if (key === "e" && location.href.match0(/nicovideo/)) { // e::enqueue
            e.preventDefault();
            var ele = document.elementFromPoint(mousex, mousey);
            var ancestorEle = getTitleFromParent(ele, 0, '//div[3]/ul[@class="list" and @data-video-list=""]/li[@data-nicoad-video=""]');
            if (!ancestorEle) return false
            let titleEle = eleget0('.//p[@class="itemTitle"]/a', ancestorEle);
            if (!titleEle) return false
            myqueue.push({ id: titleEle.href.replace(/^.+\/watch\/|\?.+/gmi, ""), title: titleEle.innerText.trim() })
            myqueue = Array.from(new Set(myqueue.map(a => JSON.stringify(a)))).map(a => JSON.parse(a));
            popup(`e:『${titleEle.textContent}』をキューに入れました(y:再生)\n${myqueue.map((c,i)=>`${1+i}) ${c.title} <span style="float:right">(${c.id})</span>`).join("\n")}`)
            return false;
          }
          if (key === "y" && !/\/watch/.test(location.href)) { // y::start playing
            e.preventDefault();
            var url = `${myqueue.map(c=>c.id).join(",")}を連続再生するURLがありません`
            alert(url)
            return false;
          }
        }
      }, false)
      return
    }
  */
  var esckey
  document.addEventListener('keydown', e => {
    if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.getAttribute('contenteditable') === 'true' || ((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 && !esckey) {
      esckey = setInterval(() => { // esc::ミニプレイヤーを常に閉じる
        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();
          clearInterval(esckey)
          esckey = 0
        }
      }, 701);
    }

    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'); //マウスが乗っている動画の枠
      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) {
          equeue.push(vID)
          equeue = [...new Set(equeue)]
          $('#immediatePlaylistButton').html(`Immediate<BR>Playlist (${equeue.length}+)`)
        }
      }

      var prevcue = eleget0('//yt-formatted-string[contains(text(),"キューに追加")]', 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[@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) && USE_IMMEDIATE_PLAYLIST) { // Alt+c::視聴中の再生リストをURLにしてコピー
      e.preventDefault();
      let eles = elegeta('//ytd-playlist-panel-video-renderer[@id="playlist-items"]/a');
      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');
        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 == 1) {
          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 {
          // プレイリストの動画の合計時間を算出、全部読み込みが終わっていないとしない
          let playtimesum = (elegeta('//ytd-playlist-panel-video-renderer[@class="style-scope ytd-playlist-panel-renderer" and @watch-color-update="" and @id="playlist-items"]').slice(0, 49).length != 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"]').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"]').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`
          }

          var title = `▶ ${indexNo+1}/${videoIDa.length} ${document.title}`
          if (videoIDaAll.length <= 50) {
            popup(kaisuu == 2 ? cb : document.title + "\n" + cb, "", "right:0em; top:0em;max-width:40%;")
            GM.setClipboard(kaisuu == 2 ? cb : document.title + "\n" + cb2 + "\n");
          } else {
            popup(kaisuu == 2 ? cb : document.title + "\n" + enumText, "", "right:0em; top:0em;max-width:40%;")
            GM.setClipboard(kaisuu == 2 ? cb : document.title + "\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() })
        }
        kaisuu = ++kaisuu % YOUTUBE_WATCH_ALTC_VARIATIONS;
        return false
      } else { //キューやプレイリスト再生状態ではない
        // makeContPlay()
        // return false
      }
    }
  }, false)

  return;

  function makeContPlay(option = "") {
    var inp = prompt(`${option=="shuffle"?"<シャッフル>\n\n":""}url Extract & Concat:\n複数のYouTubeの動画URLから連続再生URLを作ってクリップボードにコピーします\nYouTubeの動画URLを何行でも貼り付けてください\nYouTubeのURLになっていない行や文字列は全て読み飛ばされ、重複した動画は削除されます\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`)
    if (inp) {
      //      var urlcap = [...inp.matchAll(/ttps?:\/\/www\.youtube\.com\/watch\?v=([a-zA-Z0-9_\-]{11})|ttps?:\/\/youtu\.be\/([a-zA-Z0-9_\-]{11})|ttps?:\/\/www\.youtube\.com\/shorts\/([a-zA-Z0-9_\-]{11})|ttps?:\/\/www\.youtube\.com\/watch_videos\?video_ids=([a-zA-Z0-9_\-,]{11,600})/gmi)].map(c => c.slice(1, 999));
      var urlcap = [];
      inp.split(/\s/).forEach(v => { urlcap = urlcap.concat(...[...v?.matchAll(/ttps?:\/\/www\.youtube\.com\/watch\?v=([a-zA-Z0-9_\-]{11})|ttps?:\/\/youtu\.be\/([a-zA-Z0-9_\-]{11})|ttps?:\/\/www\.youtube\.com\/shorts\/([a-zA-Z0-9_\-]{11})|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 = ``
          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`
          }
          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 href=${url}>${title}</a></div>`).hide(0).insertAfter($('#logo')).show(150).delay(9999).hide(250, function() { $(this).remove() })

            //            GM.setClipboard(title + "\n" + url + "\n")
            GM.setClipboard(enumText)
            //            popup(title + "\n" + url, "", "right:0em; top:0em;max-width:40%;")
            popup(enumText, "", "right:0em; top:0em;max-width:40%;")
          }
        }
        //  }
      }
    }
  }

  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列
        '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/.*/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) {
      if (USE_IMMEDIATE_PLAYLIST) {
        $('#immediatePlaylistButton,#extractAndConcatButton').remove();

        $('ytd-topbar-logo-renderer#logo:not([data-rcli])').on("contextmenu", () => { makeContPlay(); 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 => {
            lmd[e.button] = setTimeout(e => {
              if (e.button == 2) { makeContPlay("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${equeue.length?" ("+equeue.length+"+)":""}</span>`)
        immediatePlaylistButton.insertAfter(place);
        immediatePlaylistButton.on("contextmenu", (e) => { playAllG(e, "shuffle"); return false; });
        immediatePlaylistButton.on("click", (e) => { playAllG(e); return false; });
        immediatePlaylistButton.on("mousemove", () => {
          var [url, len, lenmax, enumURLsa] = getUrla();
          //          $('#immediatePlaylistButton').attr("title", `クリックで画面に出ている動画を限定公開プレイリストにして再生 (${len}/${lenmax})\n(右クリックだとシャッフル、Ctrl+で新しいタブで開く、Shift+で51件以上も分割して開く)\nGenerate playlist from all displayed videos and open immediately (right-click to shuffle)\n\n${url}` + (lenmax > 50 ? `\n\nShift+:(${Math.ceil(lenmax/50)} tabs)\n${enumURLsa.join("\n")}` : ``));
          $('#immediatePlaylistButton').attr("title", `クリックで画面に出ている動画を限定公開プレイリストにして再生 (${len}/${lenmax})\n(右クリックだとシャッフル、Ctrl+で新しいタブで開く)\nGenerate playlist from all displayed videos and open immediately (right-click to shuffle)\n\n${url}`);
          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)" 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;
          let currentLength = elegeta('yt-icon.style-scope.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 (USE_IMMEDIATE_PLAYLIST == 2 || (USE_IMMEDIATE_PLAYLIST == 1 && confirm(`下記を開きます。よろしいですか?` + (`(${len})\n\n${url}`)))) {
        //if (e.shiftKey) { enumURLsa.forEach((v, i) => { setTimeout(() => window.open(v), i * 5000) }) } else
        if (e.ctrlKey) { window.open(url) } else { location.href = url }
      }
    //alert(`(${len})\n${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/"]: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 = [...videoIDa]
      if (videoIDa.length) {
        videoIDa = ((option === "shuffle") ? shuffle(videoIDa) : videoIDa)
        var videoIDa = [...new Set([...equeue, ...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) {
    pauseVideo(); //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"]', document.body, 1)
    //    let videoEle = elegeta('//yt-icon[@class="style-scope ytd-menu-renderer"]');
    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 ((i++) >= 199) break; // キューは200件までしか入らないので時間節約
    }
    //    d += 100 + wait * Math.min(7000, Math.max(2000, waitLast)) / 1000 + videoLength / 3;
    d += 200 + wait * Math.min(7000, Math.max(2000, waitLast)) / 1000 + videoLength / 3;
    d = Math.max(d, CHROME ? 1500 : 800)
    //    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);
    //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");
    //if (!(location.href.match(/\/watch\?v=/))) {cli('button.ytp-miniplayer-expand-watch-page-button.ytp-button.ytp-miniplayer-button-top-left', d, "infinity",()=>{
    if (!(location.href.match(/\/watch\?v=/))) {
      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, "infinity", () => {
        //      cli('//div[@class="ytp-miniplayer-scrim"]/button[@aria-label="拡大(i)"]|//div[@class="ytp-miniplayer-scrim"]/button[@aria-label="Expand (i)"]', d, "infinity", () => {
        //    d += 250 + wait * Math.min(7000, Math.max(2000, waitLast)) / 1000 + videoLength / 3;
        d += 100 + 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)"]', 100, "infinity");
        cli('//div[@class="ytp-miniplayer-scrim"]/button[@aria-label="拡大(i)"]|//div[@class="ytp-miniplayer-scrim"]/button[@aria-label="Expand (i)"]', 200, "infinity")
        //    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");
      })
    } 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(); } else if (mode === "infinity") { cli(xpath, 200, mode) }
      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 cliasync(xpath, wait) { // mode: infinity:押せるまで監視し続ける
        setTimeout(() => {
          let ele = eleget0(xpath);
          if (ele) {
            ele.click();
            return new Promise(resolve => {
              resolve(1);
            });
          } else { cli(xpath, 17, mode) }
        }, wait)
        //    if (eleget0(xpath)) { return true } else { return false }
      }*/

  function elegeta(xpath, node = document, onlyVisible = 1) {
    if (!xpath) return [];
    if (!/^\.?\//.test(xpath)) return /:visible$/.test(xpath) ? [...node.querySelectorAll(xpath.replace(/:visible$/, ""))].filter(e => e.offsetHeight) : [...node.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 || !node) return null;
    if (!/^\.?\//.test(xpath)) return /:visible$/.test(xpath) ? [...node.querySelectorAll(xpath.replace(/:visible$/, ""))].filter(e => e.offsetHeight)[0] ?? null : node.querySelector(xpath.replace(/:visible$/, ""));
    try {
      var ele = document.evaluate("." + xpath.replace(/:visible$/, ""), 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('')
  }
})();