Youtube 封面

獲取影片封面!

  1. // ==UserScript==
  2. // @name Youtube 封面
  3. // @name:en Youtube Cover
  4. // @name:zh-CN Youtube 封面
  5. // @namespace http://tampermonkey.net/
  6. // @version 1.4.6
  7. // @description 獲取影片封面!
  8. // @description:en Get the cover of youtube video!
  9. // @description:zh-CN 获取视频封面!
  10. // @author Anong0u0
  11. // @match *://*.youtube.com/*
  12. // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
  13. // @grant GM_setValue
  14. // @grant GM_getValue
  15. // @grant GM_registerMenuCommand
  16. // @noframes
  17. // @license MIT License
  18. // ==/UserScript==
  19.  
  20.  
  21. const delay = (ms = 0) => new Promise((r)=>{setTimeout(r, ms)})
  22.  
  23. const waitElementLoad = (elementSelector, selectCount = 1, tryTimes = 1, interval = 0) =>
  24. {
  25. return new Promise(async (resolve, reject)=>
  26. {
  27. let t = 1, result;
  28. while(true)
  29. {
  30. if(selectCount != 1) {if((result = document.querySelectorAll(elementSelector)).length >= selectCount) break;}
  31. else {if(result = document.querySelector(elementSelector)) break;}
  32.  
  33. if(tryTimes>0 && ++t>tryTimes) return reject(new Error("Wait Timeout"));
  34. await delay(interval);
  35. }
  36. resolve(result);
  37. })
  38. }
  39.  
  40. if (window.trustedTypes)
  41. {
  42. const policy = trustedTypes.createPolicy("ytCover", {createHTML: (string) => string,});
  43. Node.prototype.setHTML = function (html) {this.innerHTML = policy.createHTML(html)}
  44. }
  45. else Node.prototype.setHTML = function (html) {this.innerHTML = html}
  46.  
  47. Node.prototype.getXY = function ()
  48. {
  49. let x = 0, y = 0, element = this;
  50. while (element)
  51. {
  52. x += element.offsetLeft - element.scrollLeft + element.clientLeft;
  53. y += element.offsetTop - element.scrollLeft + element.clientTop;
  54. element = element.offsetParent;
  55. }
  56. return {X: x, Y: y}
  57. }
  58.  
  59. const checkImg = async (url) => await fetch(url, { method: "HEAD" })
  60. .then(response => response.ok)
  61. .catch(() => false)
  62.  
  63.  
  64. const main = () =>
  65. {
  66. const div = document.createElement("div");
  67. div.style.marginLeft = "3em";
  68. div.setHTML(`
  69.  
  70. <!-- css -->
  71. <style>
  72. #ytCover {
  73. text-decoration: none;
  74. font-size: 2em;
  75. font-weight: bold;
  76. font-family: Roboto, Arial, sans-serif;
  77. color: var(--yt-spec-text-primary);
  78. }
  79.  
  80. .list {
  81. background-color: var(--yt-spec-brand-background-primary);
  82. border: 1px solid var(--yt-spec-10-percent-layer);
  83. padding: 0.5em 0;
  84. position: fixed;
  85. z-index: 114514;
  86. max-height: 40em;
  87. font-size: 10px
  88. }
  89.  
  90. .linkBtn {
  91. text-decoration: none;
  92. }
  93.  
  94. .list-item {
  95. text-align: center;
  96. font-size: 1.5em;
  97. color: var(--yt-spec-text-primary);
  98. background-color: var(--yt-spec-brand-background-primary);
  99. height: 2.5em;
  100. line-height: 2.5em;
  101. }
  102. .list-item:hover {
  103. background: #AAA;
  104. box-shadow: 0 4px 5px rgba(0, 0, 0, 0.2);
  105. }
  106.  
  107. .slide {
  108. cursor: default
  109. }
  110.  
  111. img#preview {
  112. position: fixed;
  113. top: 50%;
  114. left: 50%;
  115. transform: translate(-50%, -50%);
  116. z-index: 2000;
  117. max-width: 100vw;
  118. max-height: 65vh;
  119. min-width: 40vw;
  120. border: 3px solid #FFF;
  121. }
  122.  
  123. .list > button {
  124. border: none;
  125. padding: unset;
  126. width: inherit;
  127. cursor: pointer;
  128. }
  129.  
  130. </style>
  131.  
  132. <!-- html -->
  133. <div>
  134. <div class="slide" id="ytCover"></div>
  135.  
  136. <div class="list" id="ytListHead" style="border-top: none; top: 4.8em; left: 19em;" hidden>
  137.  
  138. <div class="list-item slide">1280x720+
  139. <div class="list" style="border-left: none; width: 11.5em" hidden>
  140. <a class="linkBtn" imgTag="maxresdefault"><div class="list-item">maxresdefault</div></a>
  141. <a class="linkBtn" imgTag="maxres1"><div class="list-item">maxres1</div></a>
  142. <a class="linkBtn" imgTag="maxres2"><div class="list-item">maxres2</div></a>
  143. <a class="linkBtn" imgTag="maxres3"><div class="list-item">maxres3</div></a>
  144. </div>
  145. </div>
  146.  
  147. <div class="list-item slide">640x480
  148. <div class="list" style="border-left: none; width: 8.5em" hidden>
  149. <a class="linkBtn" imgTag="sddefault"><div class="list-item">sddefault</div></a>
  150. <a class="linkBtn" imgTag="sd1"><div class="list-item">sd1</div></a>
  151. <a class="linkBtn" imgTag="sd2"><div class="list-item">sd2</div></a>
  152. <a class="linkBtn" imgTag="sd3"><div class="list-item">sd3</div></a>
  153. </div>
  154. </div>
  155.  
  156. <div class="list-item slide">480x360
  157. <div class="list" style="border-left: none; width: 8.5em" hidden>
  158. <a class="linkBtn" imgTag="hqdefault"><div class="list-item">hqdefault</div></a>
  159. <a class="linkBtn" imgTag="hq1"><div class="list-item">hq1</div></a>
  160. <a class="linkBtn" imgTag="hq2"><div class="list-item">hq2</div></a>
  161. <a class="linkBtn" imgTag="hq3"><div class="list-item">hq3</div></a>
  162. </div>
  163. </div>
  164.  
  165. <div class="list-item slide">320x180
  166. <div class="list" style="border-left: none; width: 8.5em" hidden>
  167. <a class="linkBtn" imgTag="mqdefault"><div class="list-item">mqdefault</div></a>
  168. <a class="linkBtn" imgTag="mq1"><div class="list-item">mq1</div></a>
  169. <a class="linkBtn" imgTag="mq2"><div class="list-item">mq2</div></a>
  170. <a class="linkBtn" imgTag="mq3"><div class="list-item">mq3</div></a>
  171. </div>
  172. </div>
  173.  
  174. <div class="list-item slide">120x90
  175. <div class="list" style="border-left: none; width: 6.5em" hidden>
  176. <a class="linkBtn" imgTag="default"><div class="list-item">default</div></a>
  177. <a class="linkBtn" imgTag="1"><div class="list-item">1</div></a>
  178. <a class="linkBtn" imgTag="2"><div class="list-item">2</div></a>
  179. <a class="linkBtn" imgTag="3"><div class="list-item">3</div></a>
  180. </div>
  181. </div>
  182.  
  183. <button class="list-item">: <span id="previewSpan" style="font-weight: bold;">On</span></button>
  184. </div>
  185. </div>`)
  186. const insertDiv = () =>
  187. {
  188. if (GM_getValue("switchButton")) document.querySelector("#end").insertAdjacentElement("afterbegin", div);
  189. else document.querySelector("#start").append(div);
  190. }
  191. insertDiv();
  192.  
  193. const ytC = document.querySelector("#ytCover");
  194. const ytLH = document.querySelector("#ytListHead");
  195. const Lang = { cover: {en:"Cover", tc:"封面", sc:"封面"},
  196. preview: {en:"Preview", tc:"圖片預覽", sc:"图片预览"},
  197. on: {en:"On", tc:"開", sc:"开"},
  198. off: {en:"Off", tc:"關", sc:"关"},
  199. switchButton: {en:"Switch Button Position", tc:"切換按鈕位置", sc:"切换按钮位置"}};
  200. const usedLang = document.documentElement.lang.match(/zh/i) ?
  201. (document.documentElement.lang.match(/cn/i) ? "sc":"tc") : "en"
  202.  
  203. GM_registerMenuCommand(Lang.switchButton[usedLang], () =>
  204. {
  205. GM_setValue("switchButton", !GM_getValue("switchButton"));
  206. insertDiv();
  207. });
  208.  
  209.  
  210. ytC.innerText = Lang.cover[usedLang];
  211. ytLH.style.width = usedLang=="en"?"10em":"10.4em";
  212.  
  213. window.onresize = () =>
  214. {
  215. ytLH.style.left = (ytC.getXY().X/10-1)+"em";
  216. if(window.innerWidth<1350)
  217. {
  218. if(window.innerWidth>850)div.style.margin = `0 ${3*((window.innerWidth-500)/850)}em`;
  219. else div.style.margin = "0 1em";
  220. }
  221. else
  222. {div.style.margin = "0 3em"}
  223. }
  224.  
  225. document.querySelectorAll(".list > .slide").forEach((e)=>
  226. {
  227. const list = e.querySelector(".list");
  228. e.onmouseenter = () =>
  229. {
  230. list.style.top = (e.getXY().Y/10-0.5)+"em";
  231. list.style.left = parseFloat(ytLH.style.left) + parseFloat(ytLH.style.width) + "em";
  232. list.hidden = false;
  233. };
  234. e.onmouseleave = () => {list.hidden = true}
  235. });
  236.  
  237. const preview = document.createElement("img");
  238. preview.id = "preview";
  239. preview.hidden = true;
  240. document.body.append(preview);
  241.  
  242. const Btns = document.querySelectorAll(".linkBtn");
  243. Btns.forEach((e)=>
  244. {
  245. e.onmouseenter = () =>
  246. {
  247. if(!GM_getValue("previewOn")) return;
  248. preview.hidden = false;
  249. preview.src = e.href;
  250. };
  251. e.onmouseleave = () => {preview.hidden = true}
  252. e.target="_blank";
  253. });
  254.  
  255. const previewBtn = document.querySelector(".list > button");
  256. previewBtn.setHTML(Lang.preview[usedLang] + previewBtn.innerHTML);
  257. const previewSpan = document.querySelector("#previewSpan");
  258. const previewBtnChange = () =>
  259. {
  260. if (GM_getValue("previewOn"))
  261. {
  262. previewSpan.style.color = "green";
  263. previewSpan.innerText = Lang.on[usedLang];
  264. }
  265. else
  266. {
  267. previewSpan.style.color = "red";
  268. previewSpan.innerText = Lang.off[usedLang];
  269. }
  270. };
  271. previewBtn.onclick = () =>
  272. {
  273. GM_setValue("previewOn", !GM_getValue("previewOn"));
  274. previewBtnChange();
  275. }
  276. previewBtnChange();
  277.  
  278.  
  279. let hide;
  280. ytC.onmouseenter = () =>
  281. {
  282. hide = false;
  283. ytLH.hidden = false;
  284. window.onresize();
  285. };
  286. ytC.onmouseleave = () =>
  287. {
  288. hide = true;
  289. delay(500).then(()=>{ytLH.hidden = hide});
  290. };
  291. ytLH.onmouseenter = () =>
  292. {
  293. hide = false;
  294. };
  295. ytLH.onmouseleave = () =>
  296. {
  297. hide = true;
  298. delay(200).then(()=>{ytLH.hidden = hide});
  299. };
  300.  
  301. let oldHref;
  302. const onPageUpdate = () =>
  303. {
  304. if (oldHref == location.href) return
  305. oldHref = location.href
  306.  
  307. console.log("[Youtube Cover] detect page updated")
  308.  
  309. const video_id = location.href.match(/(?<=v=)[^&]{11}/);
  310. ytC.hidden = !video_id;
  311. if (!video_id) return;
  312.  
  313. document.querySelectorAll(".list-item > .list").forEach((e)=>
  314. {
  315. e.parentNode.hidden = true;
  316. e.querySelectorAll(".linkBtn").forEach((forEachBtn)=>
  317. {
  318. const imgSizeSpec = forEachBtn.getAttribute("imgTag") ?? forEachBtn.innerText
  319. checkImg(`https://i.ytimg.com/vi/${video_id}/${imgSizeSpec}.jpg`).then((notHide)=>
  320. {
  321. forEachBtn.hidden = !notHide;
  322. if(notHide) e.parentNode.hidden = false;
  323. });
  324. forEachBtn.href = `https://i.ytimg.com/vi/${video_id}/${imgSizeSpec}.jpg`
  325. });
  326. });
  327. }
  328.  
  329. document.addEventListener("yt-rendererstamper-finished", onPageUpdate)
  330. document.addEventListener("yt-page-type-changed", onPageUpdate)
  331. document.addEventListener("yt-navigate-start", onPageUpdate)
  332. waitElementLoad("yt-page-navigation-progress",1,20,250)
  333. .then((e)=>{new MutationObserver(onPageUpdate).observe(e, {attributes: true})})
  334.  
  335. console.log("[Youtube Cover] done");
  336. }
  337.  
  338. console.log("[Youtube Cover] loading");
  339. waitElementLoad("#start", 1, 10, 300).then(main)
  340.  
  341.  
  342.  
  343.  
  344.