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

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

بۇ قوليازمىنى قاچىلاش؟
ئاپتورنىڭ تەۋسىيەلىگەن قوليازمىسى

سىز بەلكىم 学園祭カレンダー絞り込み نى ياقتۇرۇشىڭىز مۇمكىن.

بۇ قوليازمىنى قاچىلاش
// ==UserScript==
// @name YouTube検索結果「全てキューに入れて再生」ボタンを追加
// @description musictonicの代わり 右クリックだとシャッフル再生 e:カーソル下の動画をキューに入れる y:再生開始 Alt+c/Ctrl+x:視聴中のキューリストをURLにしてコピー
// @version      0.1.43
// @run-at document-idle
// @match *://www.youtube.com/*
// @match *://www.youtube.com/
// @match file:///*
// @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
// @grant GM.addStyle
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_deleteValue
// @noframe
// @namespace https://greasyfork.org/users/181558
// ==/UserScript==
// @match *://*/*

(function() {
  const DEBUG = 0; // 1:wait値を表示
  const USE_INSTANT_PLAYLIST = 0; // 0:機能6-8を無効にする 1:有効にし使用時に確認を表示する 2:有効にし確認しない
  const YOUTUBE_WATCH_ALTC_VARIATIONS = 3; // 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 = 1; // 1:検索結果に割り込む「あなたへのおすすめ」「他の人はこちらも視聴しています」「家にいながら学ぶ」等を隠す 0:無効
  const PRESERVE_INDEX = 0; // 1:機能6-8で最初に再生するトラックを保持
  const INCLUDE_REEL_SHORTS = 1; // 1:機能6-8でリール棚のShorts動画を含める
  const USE_PLAYALL = 1; // 1:PlayAllボタンを有効 0:無効
  const YOUTUBE_WATCH_ALTC_EMBED_PLAYER_SIZE = `width="498" height="280"`; // ALT+C3回目の埋め込みプレイヤーのサイズ指定 322x181~
  const SEARCH_RESULTS_THUMBNAIL_VIDEOS_WIDTH = "12.5em"; // 検索結果の動画のサムネイルのサイズ "":無効
  const SEARCH_RESULTS_THUMBNAIL_SHORTS_HEIGHT = "17em"; // 検索結果のshortsのサムネイルのサイズ "":無効
  const WAIT_LOADING = 0; // PLAY ALL押下時に読み込みを待つ
  const EXPERIMENTAL_ALTERNATIVE_URL_FOR_INSTANT_PLAYLIST = 1; // 2:Instant Playlist用のURLを不具合回避のembed版にして遷移するようにする 1:embed版を開いた時通常の視聴ページに遷移する機能だけオン

  const IPURL = EXPERIMENTAL_ALTERNATIVE_URL_FOR_INSTANT_PLAYLIST >= 2 ? `https://www.youtube.com/embed/?playlist=` : `https://www.youtube.com/watch_videos?video_ids=`;
  var SHORTS = INCLUDE_REEL_SHORTS ? ",ytd-reel-item-renderer.style-scope.yt-horizontal-list-renderer div div a,ytd-rich-grid-slim-media div div a[href*='/shorts/']" : "";
  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/);

  let GF = {}
  var videoDisplayedLast = 0;
  var mllID = 0;
  var kaisuu = 0;
  var equeueIP = []
  var equeue = []

  var playAllCount, playAllCount2;
  var myqueue = [];

  let addstyle = {
    added: [],
    add: function(str) {
      if (this.added.some(v => v[1] === str)) return;
      var S = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" //      var S="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"
      var d = Date.now()
      var uid = Array.from(Array(12)).map(() => S[Math.floor((d + Math.random() * S.length) % S.length)]).join('')
      document.head.insertAdjacentHTML("beforeend", `<style id="${uid}">${str}</style>`);
      this.added.push([uid, str]);
      return uid;
    },
    remove: function(str) { // str:登録したCSSでもaddでreturnしたuidでも良い
      let uid = this.added.find(v => v[1] === str || v[0] === str)?.[0]
      if (uid) {
        eleget0(`#${uid}`)?.remove()
        this.added = this.added.filter(v => v[0] !== uid)
      }
    }
  }

  if ((window.parent == window) && EXPERIMENTAL_ALTERNATIVE_URL_FOR_INSTANT_PLAYLIST >= 1 && lh(/https:\/\/www.youtube.com\/embed\/\?playlist\=/)) { // 匿名プレイリストの埋め込み用ページだったら正規視聴ページに遷移
    GF.avoidID = setInterval(() => {
      let err = eleget0('//div[contains(@class,"ytp-error-content-wrap")]/div/span[text()="動画を再生できません"]|//div[@class="ytp-error-content-wrap-reason"]/span[text()="Video unavailable"]') // 多分外部サイトでの埋め込み再生を禁止している動画
      if (err) {
        clearInterval(GF.avoidID)
        if (Math.random() > 0.33) { // 2/3の確率で順番を変えてみる(1つめを埋め込み禁止動画じゃなくせるかも)
          var sm = location.href.split(/\s/).map(v => { return [...v?.matchAll(/^(?:h?t?tps?:\/\/)?(?:youtu\.be\/|(?:m\.|www\.|)?youtube\.com\/(?:shorts\/|watch\?v=|embed\/|live\/))([a-zA-Z0-9_\-]{11})(?![a-zA-Z0-9_\-]{1})|^(?:h?t?tps?:\/\/)?www\.youtube\.com\/(?:watch_videos\?video_ids=|embed\/\?playlist=)([a-zA-Z0-9_\-,]{11,600})/gmi)]?.map(c => c.slice(1, 999)) })?.flat()?.flat()?.map(v => v?.split(","))?.flat()?.filter(c => /^[a-zA-Z0-9_\-]{11}$/.test(c)) // 書式が混在していても登場順に収納する
          sm = sm.map(a => ({ rnd: Math.random(), val: a })).sort((a, b) => a.rnd - b.rnd).map(a => a.val);
          location.href = `https://www.youtube.com/embed/?playlist=${sm.join(",")}`;
        } else {
          location.href = location.href?.replace(/https:\/\/www.youtube.com\/embed\/\?playlist\=/, "https://www.youtube.com/watch_videos?video_ids=");
        }
        return;
      }
      let err2 = eleget0('//div[contains(@class,"ytp-error-content-wrap-reason")]/span[text()="この動画は再生できません"]|//div[contains(@class,"ytp-error-content-wrap-reason")]/span[contains(text(),"This video is unavailable")]') // 多分TLGGが間に合ってない
      if (err2) {
        clearInterval(GF.avoidID)
        if (pref("lastURL") != location.href) {
          pref("lastURL", location.href);
          begin(document.body, `<h2>Wait a few seconds...</h2>`)
          setTimeout(() => location.reload(), 2000);
          return
        }
        alert("数秒待ってリロードしてみると良いかもしれません")
        return;
      }
      let t = eleget0('a.ytp-title-link.yt-uix-sessionlink')?.href?.match0(/^https:\/\/www\.youtube\.com\/watch\?list=.*$/)
      if (t) {
        clearInterval(GF.avoidID)
        location.href = t;
        return;
      }
    }, 333)
  } else pref("lastURL", "");

  if (USE_INSTANT_PLAYLIST) {
    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 === "Shift+Y") { // Shift+Y::
        let option = "";
        let inp = [...elegeta('a:visible').map(e => e.href).filter(v => /youtube\.com|youtu\.be/.test(v)), ...document?.body?.innerText?.split(/\n|\s/)?.filter(v => /youtube\.com|youtu\.be/.test(v)), ...elegeta('iframe[src*="youtube"]').map(e => e?.src)].join(" ");
        if (inp || 1) {
          //          var urlcap = [];
          var urlcap = inp.split(/\s/).map(v => { return [...v?.matchAll(/^(?:h?t?tps?:\/\/)?(?:youtu\.be\/|(?:m\.|www\.|)?youtube\.com\/(?:shorts\/|watch\?v=|embed\/|live\/))([a-zA-Z0-9_\-]{11})(?![a-zA-Z0-9_\-]{1})|^(?:h?t?tps?:\/\/)?www\.youtube\.com\/(?:watch_videos\?video_ids=|embed\/\?playlist=)([a-zA-Z0-9_\-,]{11,600})/gmi)]?.map(c => c.slice(1, 999)) })?.flat()?.flat()?.map(v => v?.split(","))?.flat()?.filter(c => /^[a-zA-Z0-9_\-]{11}$/.test(c)) // 書式が混在していても登場順に収納する
          //        inp.split(/\s/).forEach(v => { urlcap = urlcap.concat(...[...v?.matchAll(/^(?:h?t?tps?:\/\/)?(?:m\.|www\.|)?youtube\.com\/(?:shorts\/|watch\?v=|embed\/|live\/)([a-zA-Z0-9_\-]{11})(?![a-zA-Z0-9_\-]{1})|^(?:h?t?tps?:\/\/)?youtu\.be\/([a-zA-Z0-9_\-]{11})(?![a-zA-Z0-9_\-]{1})|^(?:h?t?tps?:\/\/)?www\.youtube\.com\/(?:watch_videos\?video_ids=|embed\/\?playlist=)([a-zA-Z0-9_\-,]{11,600})/gmi)].map(c => c.slice(1, 999))).filter(w => w) }) // 書式が混在していても登場順に収納する
          inp = null;
          if (urlcap?.length || 1) {
            let urla = urlcap //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 = `${IPURL}${urla2.join(",")}`
            var enumUrl = []
            for (let u = 0; u < urla2.length / 50; u++) {
              enumUrl.push(`${IPURL}${ (urla2.slice(u*50, u*50+50).join(",") ) }`)
            }
            let [url0, urls, videos] = urlExtractAndConcat("", urllen2 ? enumUrl.join(" \n\n") : "", urla2?.length);
            if (urls?.length) {
              setTimeout(() => {
                confirm(`${videos}個の動画を${urls?.length}つのタブで開きますか?\n\n${urls?.join("\n\n")}`) && //GM.openInTab(enumUrl[0], true);
                  openUrls(escape(JS(urls)))
                return;
              }, 222)
            }
          }
        }
      }
    })
    if (!ld("youtube.com")) return; // youtube以外はここまで
  }

  // プレイリストの動画の合計時間を算出、全部読み込みが終わっていないとしない
  function gettotal(max = 99999) {
    let playlistItemLen = elegeta('ytd-playlist-panel-video-renderer#playlist-items.style-scope.ytd-playlist-panel-renderer:visible').slice(0, max).length
    if (playlistItemLen) {
      let palylistTime = elegeta('ytd-playlist-panel-video-renderer#playlist-items.style-scope.ytd-playlist-panel-renderer div.ytd-thumbnail-overlay-time-status-renderer:visible').slice(0, max)
      if (palylistTime?.length == playlistItemLen) {
        let sum = palylistTime.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)
        let total = `${zeropad(2,sum/60/60|0)}:${zeropad(2,sum/60%60|0)}:${zeropad(2,sum%60)}`
        return total;
      }
    }
    return "";
  }
  setInterval(() => {
    if (!GF.time && lh(/^https:\/\/www\.youtube\.com\/watch\?v=/)) {
      let total = gettotal()
      if (total) {
        eleget0('div#publisher-container.style-scope.ytd-playlist-panel-renderer:visible').insertAdjacentHTML("beforeend", `<div id="playlisttotaltime" style='margin: auto auto auto 1em; font-size: 1.3rem; font-weight: 400; color: var(--yt-spec-text-secondary); font-family: "Roboto","Arial",sans-serif;'>${total}</div>`);
        GF.time = 1;
      }
    }
  }, 2000);

  ["yt-navigate-finish", "yt-playlist-data-updated"].forEach(v => {
    window.addEventListener(v, () => {
      GF.time = 0
      eleget0('#playlisttotaltime')?.remove()
    })
  })

  function zeropad(pad, num) {
    return String(num)?.padStart(pad, "0")
  }

  // Enhancer for YouTubeのミニプレイヤーをカーソルを避けるようにする
  GF.avoidminiplayer = 0
  let css = '#efyt-progress,body.efyt-mini-player._top-right #movie_player:not(.ytp-fullscreen),body.efyt-mini-player._bottom-right #movie_player:not(.ytp-fullscreen){left:1em !important; right:auto !important;}'
  document.addEventListener("mousemove", e => {
    if (GF.avoidminiplayer && !e?.target?.closest('ytd-watch-next-secondary-results-renderer.style-scope.ytd-watch-flexy')) {
      GF.avoidminiplayer = 0
      addstyle.remove(css)
      return;
    } else
    if (!GF.avoidminiplayer && e?.target?.closest('ytd-watch-next-secondary-results-renderer.style-scope.ytd-watch-flexy')) { // .closestの方が:hoverより40倍ぐらい速い
      addstyle.add(css)
      GF.avoidminiplayer = 1
    }
  })

  SEARCH_RESULTS_THUMBNAIL_VIDEOS_WIDTH && GM.addStyle(`ytd-search ytd-video-renderer ytd-thumbnail.ytd-video-renderer,ytd-search ytd-playlist-thumbnail,div#avatar-section.style-scope.ytd-channel-renderer{max-width: ${SEARCH_RESULTS_THUMBNAIL_VIDEOS_WIDTH} !important;}`);
  SEARCH_RESULTS_THUMBNAIL_SHORTS_HEIGHT && GM.addStyle(`ytd-search .yt-horizontal-list-renderer .yt-core-image--content-mode-scale-aspect-fill { object-fit: contain; } ytd-search .yt-horizontal-list-renderer ytd-thumbnail.ytd-reel-item-renderer{max-height:${SEARCH_RESULTS_THUMBNAIL_SHORTS_HEIGHT} !important;}`);

  //URLの変化を監視
  var href = location.href;
  var observer = new MutationObserver(function(mutations) {
    if (href !== location.href) {
      href = location.href;
      $('#playAllButton,#instantPlaylistButton').remove();
      setTimeout(() => {
        GF.lastinner = "";
        run()
      }, 1500);
    }
  });
  observer.observe(document, { childList: true, subtree: true });
  setTimeout(() => { run(); }, 1009);
  HIDE_SUGGEST && GM.addStyle(`ytd-search ytd-shelf-renderer,ytd-search ytd-horizontal-card-list-renderer{display:none !important;}`)
  //setInterval(() => { hideSuggest() }, 1511);

  if (AGREE_TO_CONTINUE_ALWAYS) {
    setInterval(() => {
      if (!lh(/youtube\.com\/watch\?v=/)) return;
      if (eleget0('YTD-APP YTD-POPUP-CONTAINER TP-YT-PAPER-DIALOG YT-CONFIRM-DIALOG-RENDERER DIV TP-YT-PAPER-DIALOG-SCROLLABLE DIV YT-FORMATTED-STRING:visible:text*=動画が一時停止されました。続きを視聴しますか|Video paused. Continue watching'))
        eleget0('//ytd-app/ytd-popup-container/tp-yt-paper-dialog[@style-target="host"]/yt-confirm-dialog-renderer/div[last()]/div[contains(@class,"buttons style-scope yt-confirm-dialog-renderer")]/yt-button-renderer[3]/yt-button-shape/button[@aria-label="Yes" or @aria-label="はい"]/yt-touch-feedback-shape/div[contains(@class,"yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response")]/div[last()]:visible')?.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
    }
  */

  // youtube検索結果画面で「ショート」や「他の人はこちらも視聴しています」類の見出しをクリックでその動画を隠したり出したり
  GM.addStyle('h2.style-scope.ytd-reel-shelf-renderer,h2.style-scope.ytd-shelf-renderer,div#title-text.style-scope.ytd-rich-list-header-renderer{cursor:pointer;}')
  GM.addStyle('.hiddenInstance{text-decoration:underline overline line-through;}')
  let hiddenTitle = new Set()
  document.addEventListener("mousedown", e => {
    if (e.button === 0 && lh(/^https:\/\/www\.youtube\.com\/results\?search_query=/) && (e?.target?.matches('h2.style-scope.ytd-reel-shelf-renderer') || e?.target?.closest('h2.style-scope.ytd-reel-shelf-renderer,h2.style-scope.ytd-shelf-renderer,div#title-text.style-scope.ytd-rich-list-header-renderer'))) {
      e.preventDefault();
      e.stopPropagation();
      if (e?.ctrlKey) {
        GF.wari = 1 - (GF?.wari || 0);
        let css = `:is(ytd-reel-shelf-renderer.style-scope.ytd-item-section-renderer,ytd-shelf-renderer.style-scope.ytd-item-section-renderer,ytd-horizontal-card-list-renderer.style-scope.ytd-item-section-renderer) :is(div#contents.style-scope.ytd-reel-shelf-renderer,ytd-vertical-list-renderer.style-scope.ytd-shelf-renderer,div#items.style-scope.ytd-horizontal-card-list-renderer,div#scroll-outer-container.style-scope.yt-horizontal-list-renderer){display:none !important; } h2.style-scope.ytd-reel-shelf-renderer,h2.style-scope.ytd-shelf-renderer,div#title-text.style-scope.ytd-rich-list-header-renderer{opacity:0.5;}`
        GF.wari ? addstyle.add(css) : addstyle.remove(css);
      } else {
        let reelinner = e?.target?.closest('ytd-reel-shelf-renderer.style-scope.ytd-item-section-renderer,ytd-shelf-renderer.style-scope.ytd-item-section-renderer,ytd-horizontal-card-list-renderer.style-scope.ytd-item-section-renderer')?.querySelector('div#contents.style-scope.ytd-reel-shelf-renderer,ytd-vertical-list-renderer.style-scope.ytd-shelf-renderer,div#items.style-scope.ytd-horizontal-card-list-renderer,div#scroll-outer-container.style-scope.yt-horizontal-list-renderer')
        if (!hiddenTitle.has(e.target)) { //.dataset.hide=((e.target?.dataset?.hide||0)+1)%2;alert(e.target.dataset.hide);
          hiddenTitle.add(e.target);
          $(reelinner).hide(111)
        } else {
          hiddenTitle.delete(e.target);
          $(reelinner).show(111) //toggleClass("hiddenInnerInstance").toggle(111)
        }
        $(e?.target?.closest('span#title,yt-formatted-string#title.style-scope.ytd-rich-list-header-renderer')).toggleClass("hiddenInstance")
      }
      return false;
    }
  })
  hoverHelp(e => e?.matches('h2.style-scope.ytd-reel-shelf-renderer') || e?.closest('h2.style-scope.ytd-reel-shelf-renderer,h2.style-scope.ytd-shelf-renderer,div#title-text.style-scope.ytd-rich-list-header-renderer') ? "クリック:隠す/再表示<br>Ctrl+クリック:この類の棚をすべて隠す/再表示" : "")

  function hoverHelp(cb) {
    let latest, helpEle;
    document.addEventListener("mousemove", e => {
      if (latest != e?.target) {
        helpEle?.remove()
        const text = cb(e.target)
        if (text) helpEle = end(document.body, `<div id="hoverHelpPopup" style="z-index:999999999; bottom:1em; right:1em; position:fixed; background-color:#ffffffe0; padding:1px 0.5em; border:1px solid #505050; font-size:15px; color:#505050; border-radius:0.75em;">${text}</div>`)
        latest = e.target
        if (document.elementFromPoint(e.clientX, e.clientY) == helpEle) {
          helpEle.style.bottom = "";
          helpEle.style.top = "1em";
        }
      }
    })
  }

  GM.addStyle('.boxatt{ background-color:#efe !important; animation: pulse 1s 1; } @keyframes pulse { 0% { box-shadow: 0 0 0 0 #00ff88f0; } 100% { box-shadow: 0 0 10px 35px #ffffff00; } } .yenClickHighlight {outline: rgba(0, 255,128,0.7) solid 4px !important; }')

  function boxatt(e) {
    [e].flat().forEach(v => {
      v.classList.add("boxatt")
      setTimeout(() => v.classList.remove("boxatt"), 1000)
    })
  }

  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 = eleget0(`:is(ytd-video-renderer,ytd-rich-item-renderer,ytd-grid-video-renderer,ytd-playlist-video-renderer,ytd-reel-item-renderer.style-scope.yt-horizontal-list-renderer,ytd-compact-video-renderer):hover`); //マウスが乗っている動画の枠
      if (box && USE_INSTANT_PLAYLIST) { //IP先頭用に独自キューを覚えておく
        var href = eleget0(':is(a[href*="/watch"],a[href*="/shorts/"])', 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)]
          $('#instantPlaylistButton').html(`Instant<BR>Playlist (${equeueIP.length}+)`)
          if (USE_INSTANT_PLAYLIST) boxatt([box, eleget0('#instantPlaylistButton')])
          //eleget0('#instantPlaylistButton')?.classList?.add("boxatt");
        }
      }

      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)
      var ances = box;

      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 = elegeta('yt-formatted-string.style-scope.ytd-menu-service-item-renderer')?.find(v => ["キューに追加", "Add to queue"].includes(v.textContent))
          //        let queue = eleget0('//span[text()="キューに追加"]|//span[text()="Add to queue"]'); //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);
        if (!USE_INSTANT_PLAYLIST) boxatt([ancestorEle, eleget0('#instantPlaylistButton')])
      }
      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 (/^Alt\+c$|^Ctrl\+x$/.test(key) && /\/watch/.test(location.href) && USE_INSTANT_PLAYLIST) { // Alt+c:: Ctrl+x:: 視聴中の再生リストを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_INSTANT_PLAYLIST) { // Alt+c::視聴中の再生リストをURLにしてコピー
      //let eles = elegeta('//ytd-playlist-panel-video-renderer[@id="playlist-items"]/a:visible');
      let eles = elegeta('//ytd-playlist-panel-video-renderer[@id="playlist-items"]/a:visible').filter(e => e?.closest('ytd-playlist-panel-video-renderer')?.style?.opacity != 0.5);
      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 = (PRESERVE_INDEX && indexNo > 0 && indexNo < 50) ? `&index=${indexNo}` : "";
        elegeta("#link4bm").forEach(e => e.remove())
        if (kaisuu == 0 || kaisuu == 2 || disp == 0) {
          // プレイリストの動画の合計時間を算出、全部読み込みが終わっていないとしない
          let playtimesum = gettotal(50)
          playtimesum = playtimesum ? " " + playtimesum : ""
          let pl = location.href.match0(/\&list=((?:PL|UU)[a-zA-Z0-9_\-]+)/); //let pl = location.href.match0(/\&list=((?:PL|UU|UL)[a-zA-Z0-9_\-]+)/);
          var cb = kaisuu == 2 ? `<div>\n<a rel="noopener noreferrer" href="${IPURL}${ (videoIDaAll?.slice(0,50)?.join(","))}">${sani(eleget0('span#video-title.style-scope.ytd-playlist-panel-video-renderer')?.textContent?.replace(/\n/gm," ")?.trim())} (${videoIDa?.length})${playtimesum?" "+playtimesum:""}</a><br>\n<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" ${YOUTUBE_WATCH_ALTC_EMBED_PLAYER_SIZE} frameborder="0"></iframe>\n</div>\n\n` + (pl ? `<div>\n<a rel="noopener noreferrer" href="https://www.youtube.com/playlist?list=${pl}">${sani(eleget0('yt-formatted-string.title.style-scope.ytd-playlist-panel-renderer.complex-string')?.textContent?.replace(/\s+|\n/gm," ")?.trim()||"")}</a><br>\n<iframe referrerpolicy="no-referrer" src="https://www.youtube.com/embed?listType=playlist&list=${pl}" id="ytplayer" type="text/html" allowfullscreen allow="picture-in-picture" ${YOUTUBE_WATCH_ALTC_EMBED_PLAYER_SIZE} frameborder="0"></iframe>\n</div>` : "") :
            //            "https://www.youtube.com/watch_videos?video_ids=" + videoIDa.join(",") + indexUrlQP; // h181px~:hd thumbnail + "&cc_load_policy=1&cc_lang_pref=jpn"
            `${IPURL}${videoIDa.join(",") + indexUrlQP}`; // h181px~:hd thumbnail + "&cc_load_policy=1&cc_lang_pref=jpn"
          //var embedHTML = `<iframe referrerpolicy="no-referrer" src="${cb}" id="ytplayer" type="text/html" width=640 height=360 frameborder=0 allowfullscreen>`; // h181px~:hd thumbnail
          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?",…":"")}\n${IPURL}${ (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 uploader = eleget0('div.ytd-channel-name > yt-formatted-string.ytd-channel-name.complex-string[dir="auto"] > a.yt-formatted-string')?.textContent?.trim() || ""
          if (!PRESERVE_INDEX) {
            let firstVideo = eleget0('ytd-playlist-panel-video-renderer.ytd-playlist-panel-renderer:nth-child(1) > a.yt-simple-endpoint > div#container.ytd-playlist-panel-video-renderer > div.editable:last-child > h4 > span.style-scope')?.textContent?.trim()
            transratedTitle = firstVideo ? firstVideo + " - YouTube" : transratedTitle;
            var uploader = eleget0('//div[@id="items" and @class="playlist-items style-scope ytd-playlist-panel-renderer"]/ytd-playlist-panel-video-renderer[1]/a[contains(@class,"yt-simple-endpoint style-scope ytd-playlist-panel-video-renderer")]/div[@id="container"]/div[@id="meta"]/div[@id="byline-container"]/span')?.textContent?.trim() || ""
          }
          let desc = indexNo !== 0 ? "" : sani((eleget0('ytd-text-inline-expander#description-inline-expander.style-scope.ytd-watch-metadata')?.innerText || "")?.replace(/\n+|\s+/gm, " ")?.slice(0, 60)?.trim())
          if (desc) desc = ` - ${desc}`
          var title = `▶ ${PRESERVE_INDEX?indexNo+1:1}/${videoIDa.length} ${transratedTitle} - ${uploader}${desc}`;
          if (disp) {
            if (videoIDaAll.length <= 50) {
              popup(kaisuu == 2 ? cb : transratedTitle + "\n" + cb, kaisuu == 2 ? "#822" : undefined, "right:0em; top:0em;max-width:40%;")
              GM.setClipboard(kaisuu == 2 ? cb : transratedTitle + "\n" + cb2 + "\n");
            } else {
              popup(kaisuu == 2 ? cb : transratedTitle + "\n" + enumText, kaisuu == 2 ? "#822" : undefined, "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')).on("click", () => pauseVideo()).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() + " - YouTube\n" + e.closest('a').href.trim().replace(/&.*/, "") + "\n" }).map(a => JSON.stringify(a)))].map(a => JSON.parse(a)).join("")
          list = [...new Set(elegeta('//h4[@class=\"style-scope ytd-playlist-panel-video-renderer\"]/span[@id=\"video-title\"]:visible').filter(e => e?.closest('ytd-playlist-panel-video-renderer')?.style?.opacity != 0.5).map(e => { return e.textContent.trim() + " - YouTube\n" + e.closest('a').href.trim().replace(/&.*/, "") + "\n" }).map(a => JSON.stringify(a)))].map(a => JSON.parse(a)) //.join("")
          if (disp) {
            popup(list.join(""), "#303060") //popup(`(${list?.length})\n`+list.join(""), "#303060")
            GM.setClipboard(list.join("") + "");
          }
          return list
        }

      }
      return cb
    } else { //キューやプレイリスト再生状態ではない
      return;
    }
  }


  function urlExtractAndConcat(option = "", ini = "", inilen = 0) { // url Extract & Concat
    let cb = ini || makeUrlFromCuelist(0)
    var inp = prompt(`${option=="shuffle"?"<シャッフル>\n\n":""}url Extract & Concat:\n複数のYouTubeの動画URLから連続再生URLを作ってクリップボードにコピーします\nYouTubeの動画URLを何行でも貼り付けてください\nYouTubeのURLになっていない行や文字列は全て読み飛ばされ、重複した動画は削除されます\n${ini?`\n今のページにあるYouTube動画URL(${inilen})が初期値として入力済みです\nこれを利用して前後に追加することも、削除して新しく入力することもできます\n\n${cb}\n`:cb?`\n今視聴中のキュー/プレイリストの動画が初期値として入力済みです\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?t?tps?:\/\/)?(?:m\.|www\.|)?youtube\.com\/(?:shorts\/|watch\?v=|embed\/|live\/)([a-zA-Z0-9_\-]{11})(?![a-zA-Z0-9_\-]{1})|^(?:h?t?tps?:\/\/)?youtu\.be\/([a-zA-Z0-9_\-]{11})(?![a-zA-Z0-9_\-]{1})|^(?:h?t?tps?:\/\/)?www\.youtube\.com\/(?:watch_videos\?video_ids=|embed\/\?playlist=)([a-zA-Z0-9_\-,]{11,600})/gmi)].map(c => c.slice(1, 999))).filter(w => w) }) // 書式が混在していても登場順に収納する
      inp = null;
      if (urlcap?.length) {
        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 = `${IPURL}${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?",…":"")}\n${IPURL}${ (urla2.slice(u*50, u*50+50).join(",") ) }\n`
            enumUrl.push(`${IPURL}${ (urla2.slice(u*50, u*50+50).join(",") ) }`)
          }
          let con = USE_INSTANT_PLAYLIST == 1 ? confirm(`${urllen}件の動画IDを抽出しました\n${urllen-urllen2}件の重複を削除しました\n\n下記(${urla2.length}件)をクリップボードにコピーしますか?\n\n${enumText}`) : 1;
          if (con) {
            if (ld("youtube.com") && eleget0("#logo")) $(`<div style="margin-left:4em;font-size:14px;" id="link4bm">${kaisuu==2?"埋め込み":"ブックマーク"}用リンク(${urla3.length})<br><a title="Shift+左クリック:以下をすべて開く(${urllen2})\n\n${enumUrl.join("\n\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()
                openUrls(e.target.dataset.urls)
                return [url, enumUrl, urllen2];
              }
            })
            GM.setClipboard(enumText)
            popup(enumText, undefined, "right:0em; top:0em;max-width:40%;")
            return [url, enumUrl, urllen2];
          }
        }
      }
    } else return ["", [], 0];
  }

  function openUrls(urls) {
    pauseVideo()
    var enumURLsa = JP(unescape(urls))
    enumURLsa.forEach((v, i) => { setTimeout(() => i == 0 ? GM.openInTab(v) : GM.openInTab(v, true), i * 5000) }) // Shift+左クリック

  }

  function hideSuggest() {
    if (HIDE_SUGGEST && location.href.indexOf('www.youtube.com/results?') !== -1) {
      [
        //      ['ytd-shelf-renderer,ytd-horizontal-card-list-renderer', // 縦1列「○○の最新の動画をお見逃しなく」「他の人はこちらも検索」
        '#contents>ytd-horizontal-card-list-renderer', // 縦1列「他の人はこちらも検索」
        'ytd-shelf-renderer', // 縦1列「他の人はこちらも視聴しています」「あなたへのおすすめ」「~の最新の動画をお見逃しなく」「関連する検索から」
        //`//span[text()="他の人はこちらのショート動画も視聴しています"]/ancestor::ytd-reel-shelf-renderer`, // 「他の人はこちらのショート動画も視聴しています」
        `ytd-reel-shelf-renderer:text*=他の人はこちらのショート動画も視聴しています`, // 「他の人はこちらのショート動画も視聴しています」
      ].forEach(xp => {
        $(elegeta(xp)).css({ "outline": "6px dashed #f00" }).hide(HIDE_SUGGEST, function() { $(this).remove() }); // 検索結果に割り込むサジェストを隠す
      });
    }
  }

  function run(node = document) {
    if (lh(/\/\/www\.youtube\.com\/(?:\?bp=.*)?$/) ||
      location.href.match(/https:\/\/www\.youtube\.com\/results\?.*(q=|search_query=)/) || 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_INSTANT_PLAYLIST) {
        $('#instantPlaylistButton,#extractAndConcatButton').remove();
        $('ytd-topbar-logo-renderer#logo:not([data-rcli])').on("contextmenu", () => { return false; }); // ytアイコン右クリック
        var e = eleget0('ytd-topbar-logo-renderer#logo:not([data-rcli])')
        if (e) {
          e.dataset.rcli = 1;
          e?.addEventListener("mouseup", e => {
            if (e.button != 2) return;
            if (e.ctrlKey || Date.now() - GF?.logocli > 400) { // ytロゴ右長押しかCtrl+ytロゴ右クリック
              urlExtractAndConcat("shuffle")
            } else {
              urlExtractAndConcat();
            }
            return false;
          })
          e?.addEventListener("mousedown", e => {
            if (e.button != 2) return;
            GF.logocli = Date.now()
          })
        }

        var instantPlaylistButton = $(`<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 instantly (right-click to shuffle)" id="instantPlaylistButton">Instant<br>Playlist${equeueIP.length?" ("+equeueIP.length+"+)":""}</span>`)
        instantPlaylistButton.insertAfter(place);
        instantPlaylistButton.on("contextmenu", e => false)
        instantPlaylistButton.on("mousedown", (e) => {
          if (e.button == 0) playAllG(e);
          if (e.button == 2) playAllG(e, "shuffle");
          return false;
        });

        instantPlaylistButton.on("mousemove", () => {
          var [url, len, lenmax, enumURLsa] = getUrla();
          $('#instantPlaylistButton').attr("title", `クリックで画面に出ている動画を限定公開プレイリストにして再生 (${len}/${lenmax})\n(右クリックだとシャッフル、Ctrl+で新しいタブで開く、Shift+で51件以上も分割して開く)\nGenerate playlist from all displayed videos and open instantly (right-click to shuffle)\n\n${enumURLsa?.join("\n\n")}`);
          return false;
        });
      }
      if (USE_PLAYALL) {
        $('#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) => {
          e.stopPropagation();
          e.preventDefault();
          return false;
        });
        playAllButton.on("mousedown", (e) => {
          e.stopPropagation();
          e.preventDefault();
          setTimeout(() => playAll(e?.button != 0 ? "shuffle" : "", e), 20);
          return false;
        });
        if (!playAllCount) {
          playAllCount = setInterval(() => {
            let len = item()?.length
            let lenall = itemWhole()?.length
            DEBUG && $(item()).css({ "outline": "3px dotted #80f" })
            DEBUG && $(itemWhole()).css({ "outline": "3px dotted #f0f" })
            let size = len == lenall ? len : `${len}/${lenall}`
            GF.inner = `Play All<br>(${size})${GF?.PAadd||""}${DEBUG ? "<br>wait:" + wait : ""}`
            if (GF.lastinner != GF.inner) $('#playAllButton').html(GF.inner);
            GF.lastinner = GF.inner;
          }, 1000);
        }
      }
    }
  }

  function item() {
    return [
      ...elegeta('ytd-video-renderer.ytd-item-section-renderer:not(:has(ytd-rich-grid-slim-media)) ytd-menu-renderer.ytd-video-renderer > yt-icon-button#button.ytd-menu-renderer > button.style-scope > yt-icon > span > div , ytd-video-renderer.ytd-item-section-renderer yt-icon.style-scope.ytd-menu-renderer > yt-icon-shape > icon-shape > div:visible') //sea::
      , ...elegeta('ytd-rich-item-renderer.ytd-rich-grid-row:has(ytd-video-meta-block.grid.ytd-rich-grid-media > div.ytd-video-meta-block > div#metadata-line.style-scope:last-of-type > span.style-scope.ytd-video-meta-block:nth-of-type(1) ):not(:has(ytd-rich-grid-slim-media)):visible').filter(e => eleget0('.ytd-menu-renderer:visible', e)) // top::
      , ...elegeta('div#menu.style-scope.ytd-compact-video-renderer > ytd-menu-renderer > yt-icon-button.dropdown-trigger.ytd-menu-renderer > button#button > yt-icon > yt-icon-shape.yt-icon:visible')
    ].filter(v => v)
  }

  function itemWhole() {
    return [...elegeta('ytd-video-renderer.ytd-item-section-renderer:not(:has(ytd-rich-grid-slim-media)):visible') // sea:
      , ...elegeta('ytd-rich-item-renderer.ytd-rich-grid-row:has(ytd-video-meta-block.grid.ytd-rich-grid-media > div.ytd-video-meta-block > div#metadata-line.style-scope:last-of-type > span.style-scope.ytd-video-meta-block:nth-of-type(1) ):not(:has(ytd-rich-grid-slim-media)):visible') // top::
      , ...elegeta('ytd-compact-video-renderer.ytd-watch-next-secondary-results-renderer:visible') // r-side:
    ].filter(v => v)
  }

  function playAllG(e, option = false) {
    setTimeout(pauseVideo, 17);
    var [url, len, lenmax, enumURLsa] = getUrla(option)
    if (len > 0)
      if (USE_INSTANT_PLAYLIST == 2 || (USE_INSTANT_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 ? GM.openInTab(v) : GM.openInTab(v, true), i * 5000) }) // Shift+左クリック
        } else
        if (e.ctrlKey) { GM.openInTab(url) } else { location.href = url }
      }
  }

  function debugEle(ele, col = "random", additionalInfo = "") {
    if (ele && (DEBUG || col.indexOf("forced") !== -1)) {
      if (col.indexOf("random") !== -1) col = '#' + (0x1000000 + (Math.random()) * 0xffffff).toString(16).substr(1, 6);
      //      if (col.indexOf("random") !== -1) col = '#' + ("000".map(c=>"89abcdef"[Math.random()*8]));
      //ele.style.outline = "3px dotted " + col;
      ele.style.boxShadow = " 0px 0px 4px 4px " + col + "30, inset 0 0 100px " + col + "20";
      ele.dataset.yododebugele = ""
      //ele.outerHTML+=additionalInfo;
    }
  }

  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${SHORTS}:visible`).filter(e => e?.closest('ytd-playlist-panel-video-renderer')?.style?.opacity != 0.5);
    if (videoEle.length) {
      DEBUG && videoEle.forEach(e => debugEle(e, "#8000ff")) //$(videoEle).css({"outline":"3px dotted #84f"})
      if (lh("youtube.com/playlist")) videoEle = videoEle.filter(e => !eleget0('//div/h2/span[@class="style-scope ytd-shelf-renderer" and contains(text(),"おすすめのプレイリスト")]', e.closest(`ytd-item-section-renderer`))) // プレイリスト画面の下に出るおすすめプレイリストを除外
      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, ...((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 = `${IPURL}${videoIDa.join(",")}`

        var enumURLsa = []
        for (let u = 0; u < videoIDaMax.length / 50; u++) {
          enumURLsa.push(`${IPURL}${ (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) }
  }

  async function playAll(option = false, ev) {
    //if(item()!=itemWhole())$(`<span id="yacwait" style="font-size:15px; width:8em;">Wait a few seconds...→</span>`).hide(0).insertBefore('#playAllButton').show(222)
    if (WAIT_LOADING) {
      if (item()?.length != itemWhole()?.length) GF.PAadd = "<br>Wait..." //⏳
      await waitTrue(() => item()?.length == itemWhole()?.length, 20000)
      GF.PAadd = "" //$('#yacwait').hide(250, function() { $(this).remove() }) //eleget0('#yacwait')?.remove()
    }

    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 box = elegeta('ytd-compact-video-renderer.style-scope.ytd-watch-next-secondary-results-renderer,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-compact-video-renderer.style-scope.ytd-watch-next-secondary-results-renderer,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)) { //console.log(wait,videoLength)
      e.click() //setTimeout(() => { e.click() }, d);
      //if (d == 0) await sleep(WAIT_FIRST + (videoLength * 2)); // ?
      await sleep(wait / 2) //setTimeout(() => {
      let queue = elegeta('yt-formatted-string.style-scope.ytd-menu-service-item-renderer')?.find(v => ["キューに追加", "Add to queue"].includes(v.textContent)) //        let queue = eleget0('//span[text()="キューに追加"]|//span[text()="Add to queue"]'); //let queue = eleget0('//yt-formatted-string[text()="キューに追加"]|//yt-formatted-string[text()="Add to queue"]');
      if (queue) queue.click();
      //}, d + wait / 2);
      await sleep(wait + (videoLength / 20)) //d += wait + (videoLength / 20);

      if (ev.ctrlKey) { //IP先頭用に独自キューを覚えておく
        var href = eleget0('//a', e?.closest('ytd-compact-video-renderer.style-scope.ytd-watch-next-secondary-results-renderer,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)]
          $('#instantPlaylistButton').html(`Instant<BR>Playlist (${equeueIP.length}+)`)
        }
      }

      if ((i++) >= 199) break; // キューは200件までしか入らないので時間節約
    }
    await sleep(200 + wait) //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) {
        await waitcli('//div[contains(@class,"ytp-miniplayer-play-button-container")]/button|//button[contains(@class,"ytp-play-button-playlist")]'); //cli('//div[contains(@class,"ytp-miniplayer-play-button-container")]/button|//button[contains(@class,"ytp-play-button-playlist")]', d, "infinity", () => {
        //await sleep(200+wait)//  d += 200 + wait * Math.min(7000, Math.max(2000, waitLast)) / 1000 + videoLength / 3;
        await waitcli('//div/button[contains(@class,"ytp-miniplayer-expand-watch-page-button")]:visible') //cli('//div/button[contains(@class,"ytp-miniplayer-expand-watch-page-button")]:visible', 300 + wait + videoLength / 3, "infinity")
        //})
      }
    } else {
      await sleep(100 + wait); //d += 100 + wait;
      eleget0('video')?.play(); //let e = eleget0('//video'); if (e) { e.play(); } //setTimeout(() => { let e = eleget0('//video'); if (e) { e.play(); } }, d);
    }
  }

  function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)) }

  async function waitcli(sel) {
    await waitTrue(() => eleget0(sel))
    eleget0(sel)?.click()
  }

  function waitTrue(testfunc, timeout = 5000) {
    return Promise.race([new Promise(resolve => setTimeout(resolve, timeout)), new Promise(resolve => {
      const interval = setInterval(() => {
        if (testfunc()) {
          clearInterval(interval);
          resolve();
        }
      }, 100);
    })]);
  }

  function shuffle(array) {
    return array.map(a => ({ rnd: Math.random(), val: a })).sort((a, b) => a.rnd - b.rnd).map(a => a.val);
  }

  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(`XPath/CSS構文にエラーがあるかもしれません\n2023/12以前にインストールしたFirefoxを使っている場合はabout:configでlayout.css.has-selector.enabled を true にすると解決するかもしれません\n\n${e}\n\n${xpath}`); return []; }
    //} 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(`XPath/CSS構文にエラーがあるかもしれません\n2023/12以前にインストールしたFirefoxを使っている場合はabout:configでlayout.css.has-selector.enabled を true にすると解決するかもしれません\n\n${e}\n\n${xpath}`); return 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 = "#6080ff", 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>")
    //alert(bgcolor)//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;  max-height:100vh; overflow-y:auto; scrollbar-width:thin; 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) }

  function lh(re) { let tmp = location.href.match(re); if (!tmp) { return null } else if (tmp.length > 1) { return tmp[1] } else return tmp[0] } // gフラグ不可
  function ld(re) { let tmp = location.hostname.match(re); if (!tmp) { return null } else if (tmp.length > 1) { return tmp[1] } else return tmp[0] } // gフラグ不可
  function sani(s) { return s?.replace(/&/g, "&amp;")?.replace(/"/g, "&quot;")?.replace(/'/g, "&#39;")?.replace(/`/g, '&#x60;')?.replace(/</g, "&lt;")?.replace(/>/g, "&gt;") || "" }

  function before(e, html) { e?.insertAdjacentHTML('beforebegin', html); return e?.previousElementSibling; }

  function begin(e, html) { e?.insertAdjacentHTML('afterbegin', html); return e?.firstChild; }

  function end(e, html) { e?.insertAdjacentHTML('beforeend', html); return e?.lastChild; }

  function after(e, html) { e?.insertAdjacentHTML('afterend', html); return e?.nextElementSibling; }

  function pref(name, store = null) { // prefs(name,data)で書き込み(数値でも文字列でも配列でもオブジェクトでも可)、prefs(name)で読み出し
    if (store === null) { // 読み出し
      let data = GM_getValue(name) || GM_getValue(name);
      if (data == undefined) return null; // 値がない
      if (data.substring(0, 1) === "[" && data.substring(data.length - 1) === "]") { // 配列なのでJSONで返す
        //        try { return JSON.parse(data || '[]'); } catch (e) {
        try {
          let a = JSON.parse(data || '[]');
          return a
        } catch (e) {
          alert("データベースがバグってるのでクリアします\n" + e);
          pref(name, []);
          return;
        }
        //      } else return data;
      } else {
        return data;
      }
    }
    if (store === "" || (Array.isArray(store) && store?.length == 0)) { // 書き込み、削除
      GM_deleteValue(name);
      return;
    } else if (typeof store === "string") { // 書き込み、文字列
      GM_setValue(name, store);
      return store;
    } else { // 書き込み、配列
      //      try { GM_setValue(name, JSON.stringify(store)); } catch (e) {
      try {
        let a = JSON.stringify(store);
        GM_setValue(name, a);
      } catch (e) {
        alert("データベースがバグってるのでクリアします\n" + e);
        pref(name, "");
      }
      return store;
    }
  }

})();