Get the cover of youtube video!
// ==UserScript== // @name Youtube 封面 // @name:en Youtube Cover // @name:zh-CN Youtube 封面 // @namespace http://tampermonkey.net/ // @version 1.4.5 // @description 獲取影片封面! // @description:en Get the cover of youtube video! // @description:zh-CN 获取视频封面! // @author Anong0u0 // @match *://*.youtube.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com // @grant GM_setValue // @grant GM_getValue // @noframes // @license MIT License // ==/UserScript== const delay = (ms = 0) => {return new Promise((r)=>{setTimeout(r, ms)})} const waitElementLoad = (elementSelector, selectCount = 1, tryTimes = 1, interval = 0) => { return new Promise(async (resolve, reject)=> { let t = 1, r###lt; while(true) { if(selectCount != 1) {if((r###lt = document.querySelectorAll(elementSelector)).length >= selectCount) break;} else {if(r###lt = document.querySelector(elementSelector)) break;} if(tryTimes>0 && ++t>tryTimes) return reject(new Error("Wait Timeout")); await delay(interval); } resolve(r###lt); }) } if (window.trustedTypes) { const policy = trustedTypes.createPolicy("ytCover", {createHTML: (string) => string,}); Node.prototype.setHTML = function (html) {this.innerHTML = policy.createHTML(html)} } else Node.prototype.setHTML = function (html) {this.innerHTML = html} Node.prototype.getXY = function () { let x = 0, y = 0, element = this; while (element) { x += element.offsetLeft - element.scrollLeft + element.clientLeft; y += element.offsetTop - element.scrollLeft + element.clientTop; element = element.offsetParent; } return {X: x, Y: y} } const checkImg = async (url) => { return await fetch(url, { method: "HEAD" }) .then(response => response.ok) .catch(() => false) } const main = () => { const div = document.createElement("div"); div.style.marginLeft = "3em"; div.setHTML(` <!-- css --> <style> #ytCover { text-decoration: none; font-size: 2em; font-weight: bold; font-family: Roboto, Arial, sans-serif; color: var(--yt-spec-text-primary); } div.list { background-color: var(--yt-spec-brand-background-primary); border: 1px solid var(--yt-spec-10-percent-layer); padding: 0.5em 0; position: fixed; z-index: ######; max-height: 40em; font-size: 10px } .linkBtn { text-decoration: none; } .list-item { text-align: center; font-size: 1.5em; color: var(--yt-spec-text-primary); background-color: var(--yt-spec-brand-background-primary); height: 2.5em; line-height: 2.5em; } .list-item:hover { background: #AAA; box-shadow: 0 4px 5px rgba(0, 0, 0, 0.2); } .slide { cursor: default } img#preview { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 2000; max-width: 100vw; max-height: 65vh; min-width: 40vw; border: 3px solid #FFF; } .list > button { border: none; padding: unset; width: inherit; cursor: pointer; } </style> <!-- html --> <div> <div class="slide" id="ytCover"></div> <div class="list" id="ytListHead" style="border-top: none; top: 4.8em; left: 19em;" hidden> <div class="list-item slide">1280x720+ <div class="list" style="border-left: none; width: 11.5em" hidden> <a class="linkBtn" imgTag="maxresdefault"><div class="list-item">maxresdefault</div></a> <a class="linkBtn" imgTag="maxres1"><div class="list-item">maxres1</div></a> <a class="linkBtn" imgTag="maxres2"><div class="list-item">maxres2</div></a> <a class="linkBtn" imgTag="maxres3"><div class="list-item">maxres3</div></a> </div> </div> <div class="list-item slide">640x480 <div class="list" style="border-left: none; width: 8.5em" hidden> <a class="linkBtn" imgTag="sddefault"><div class="list-item">sddefault</div></a> <a class="linkBtn" imgTag="sd1"><div class="list-item">sd1</div></a> <a class="linkBtn" imgTag="sd2"><div class="list-item">sd2</div></a> <a class="linkBtn" imgTag="sd3"><div class="list-item">sd3</div></a> </div> </div> <div class="list-item slide">480x360 <div class="list" style="border-left: none; width: 8.5em" hidden> <a class="linkBtn" imgTag="hqdefault"><div class="list-item">hqdefault</div></a> <a class="linkBtn" imgTag="hq1"><div class="list-item">hq1</div></a> <a class="linkBtn" imgTag="hq2"><div class="list-item">hq2</div></a> <a class="linkBtn" imgTag="hq3"><div class="list-item">hq3</div></a> </div> </div> <div class="list-item slide">320x180 <div class="list" style="border-left: none; width: 8.5em" hidden> <a class="linkBtn" imgTag="mqdefault"><div class="list-item">mqdefault</div></a> <a class="linkBtn" imgTag="mq1"><div class="list-item">mq1</div></a> <a class="linkBtn" imgTag="mq2"><div class="list-item">mq2</div></a> <a class="linkBtn" imgTag="mq3"><div class="list-item">mq3</div></a> </div> </div> <div class="list-item slide">120x90 <div class="list" style="border-left: none; width: 6.5em" hidden> <a class="linkBtn" imgTag="default"><div class="list-item">default</div></a> <a class="linkBtn" imgTag="1"><div class="list-item">1</div></a> <a class="linkBtn" imgTag="2"><div class="list-item">2</div></a> <a class="linkBtn" imgTag="3"><div class="list-item">3</div></a> </div> </div> <button class="list-item">: <span id="previewSpan" style="font-weight: bold;">On</span></button> </div> </div>`) document.querySelector("#start").append(div); const ytC = document.querySelector("#ytCover"); const ytLH = document.querySelector("#ytListHead"); const Lang = { cover: {en:"Cover", tc:"封面", sc:"封面"}, preview: {en:"Preview", tc:"圖片預覽", sc:"图片预览"}, on: {en:"On", tc:"開", sc:"开"}, off: {en:"Off", tc:"關", sc:"关"}}; const usedLang = document.querySelector("html").lang.match(/zh/) ? (document.querySelector("html").lang.match(/CN/) ? "sc":"tc") : "en" ytC.innerText = Lang.cover[usedLang]; ytLH.style.width = usedLang=="en"?"10em":"10.4em"; window.onresize = () => { ytLH.style.left = (ytC.getXY().X/10-1)+"em"; if(window.innerWidth<1350) { if(window.innerWidth>850)div.style.marginLeft = 3*((window.innerWidth-500)/850) + "em"; else div.style.marginLeft = "1em"; } else {div.style.marginLeft = "3em"} } document.querySelectorAll(".list > .slide").forEach((e)=> { const list = e.querySelector(".list"); e.onmouseenter = () => { list.style.top = (e.getXY().Y/10-0.5)+"em"; list.style.left = parseFloat(ytLH.style.left) + parseFloat(ytLH.style.width) + "em"; list.hidden = false; }; e.onmouseleave = () => {list.hidden = true} }); const preview = document.createElement("img"); preview.id = "preview"; preview.hidden = true; document.body.append(preview); const Btns = document.querySelectorAll(".linkBtn"); Btns.forEach((e)=> { e.onmouseenter = () => { if(!GM_getValue("previewOn")) return; preview.hidden = false; preview.src = e.href; }; e.onmouseleave = () => {preview.hidden = true} e.target="_blank"; }); const previewBtn = document.querySelector(".list > button"); previewBtn.setHTML(Lang.preview[usedLang] + previewBtn.innerHTML); const previewSpan = document.querySelector("#previewSpan"); const previewBtnChange = () => { if (GM_getValue("previewOn")) { previewSpan.style.color = "green"; previewSpan.innerText = Lang.on[usedLang]; } else { previewSpan.style.color = "red"; previewSpan.innerText = Lang.off[usedLang]; } }; previewBtn.onclick = () => { GM_setValue("previewOn", !GM_getValue("previewOn")); previewBtnChange(); } previewBtnChange(); let hide; ytC.onmouseenter = () => { hide = false; ytLH.hidden = false; }; ytC.onmouseleave = () => { hide = true; delay(500).then(()=>{ytLH.hidden = hide}); }; ytLH.onmouseenter = () => { hide = false; }; ytLH.onmouseleave = () => { hide = true; delay(200).then(()=>{ytLH.hidden = hide}); }; let oldHref; const onPageUpdate = () => { if (oldHref == location.href) return oldHref = location.href console.log("[Youtube Cover] detect page updated") const video_id = location.href.match(/(?<=v=)[^&]{11}/); ytC.hidden = !video_id; if (!video_id) return; document.querySelectorAll(".list-item > .list").forEach((e)=> { e.parentNode.hidden = true; e.querySelectorAll(".linkBtn").forEach((forEachBtn)=> { const imgSizeSpec = forEachBtn.getAttribute("imgTag") ?? forEachBtn.innerText checkImg(`https://i.ytimg.com/vi/${video_id}/${imgSizeSpec}.jpg`).then((notHide)=> { forEachBtn.hidden = !notHide; if(notHide) e.parentNode.hidden = false; }); forEachBtn.href = `https://i.ytimg.com/vi/${video_id}/${imgSizeSpec}.jpg` }); }); } document.addEventListener("yt-rendererstamper-finished", onPageUpdate) document.addEventListener("yt-page-type-changed", onPageUpdate) document.addEventListener("yt-navigate-start", onPageUpdate) waitElementLoad("yt-page-navigation-progress",1,20,250) .then((e)=>{new MutationObserver(onPageUpdate).observe(e, {attributes: true})}) console.log("[Youtube Cover] done"); } console.log("[Youtube Cover] loading"); waitElementLoad("#start", 1, 10, 300).then(main)