YouTube Live DateTime Tooltip

Make a tooltip to show the actual date and time for livestream

  1. // ==UserScript==
  2. // @name YouTube Live DateTime Tooltip
  3. // @namespace UserScript
  4. // @match https://www.youtube.com/*
  5. // @version 0.1.5
  6. // @license MIT
  7. // @author CY Fung
  8. // @run-at document-start
  9. // @grant none
  10. // @unwrap
  11. // @inject-into page
  12. // @description Make a tooltip to show the actual date and time for livestream
  13. // ==/UserScript==
  14.  
  15. ((__CONTEXT__) => {
  16.  
  17. const { Promise, requestAnimationFrame } = __CONTEXT__;
  18.  
  19. const isPassiveArgSupport = (typeof IntersectionObserver === 'function');
  20. const bubblePassive = isPassiveArgSupport ? { capture: false, passive: true } : false;
  21. const capturePassive = isPassiveArgSupport ? { capture: true, passive: true } : true;
  22.  
  23. const insp = o => o ? (o.polymerController || o.inst || o || 0) : (o || 0);
  24.  
  25. let pageFetchedDataLocal = null;
  26. document.addEventListener('yt-page-data-fetched', (evt) => {
  27. pageFetchedDataLocal = evt.detail;
  28.  
  29.  
  30. }, bubblePassive);
  31.  
  32. function getFormatDates() {
  33.  
  34. if (!pageFetchedDataLocal) return null;
  35.  
  36. const formatDates = {}
  37. try {
  38. formatDates.publishDate = pageFetchedDataLocal.pageData.playerResponse.microformat.playerMicroformatRenderer.publishDate
  39. } catch (e) { }
  40. // 2022-12-30
  41.  
  42. try {
  43. formatDates.uploadDate = pageFetchedDataLocal.pageData.playerResponse.microformat.playerMicroformatRenderer.uploadDate
  44. } catch (e) { }
  45. // 2022-12-30
  46.  
  47. try {
  48. formatDates.publishDate2 = pageFetchedDataLocal.pageData.response.contents.twoColumnWatchNextResults.results.results.contents[0].videoPrimaryInfoRenderer.dateText.simpleText
  49. } catch (e) { }
  50. // 2022/12/31
  51.  
  52. if (typeof formatDates.publishDate2 === 'string' && formatDates.publishDate2 !== formatDates.publishDate) {
  53. formatDates.publishDate = formatDates.publishDate2
  54. formatDates.uploadDate = null
  55. }
  56.  
  57. try {
  58. formatDates.broadcastBeginAt = pageFetchedDataLocal.pageData.playerResponse.microformat.playerMicroformatRenderer.liveBroadcastDetails.startTimestamp
  59. } catch (e) { }
  60. try {
  61. formatDates.broadcastEndAt = pageFetchedDataLocal.pageData.playerResponse.microformat.playerMicroformatRenderer.liveBroadcastDetails.endTimestamp
  62. } catch (e) { }
  63. try {
  64. formatDates.isLiveNow = pageFetchedDataLocal.pageData.playerResponse.microformat.playerMicroformatRenderer.liveBroadcastDetails.isLiveNow
  65. } catch (e) { }
  66.  
  67.  
  68. return formatDates;
  69. }
  70.  
  71.  
  72.  
  73. function createElement() {
  74.  
  75. /** @type {HTMLElement} */
  76. const ytdWatchFlexyElm = document.querySelector('ytd-watch-flexy');
  77. if (!ytdWatchFlexyElm) return;
  78. const ytdWatchFlexyCnt = insp(ytdWatchFlexyElm);
  79. let newPanel = ytdWatchFlexyCnt.createComponent_({
  80. "component": "ytd-button-renderer",
  81. "params": {
  82. buttonTooltipPosition: "top",
  83. systemIcons: false,
  84. modern: true,
  85. forceIconButton: false,
  86.  
  87. }
  88. }, "ytd-engagement-panel-section-list-renderer", true) || 0;
  89. const newPanelHostElement = newPanel.hostElement || newPanel || 0;
  90. const newPanelCnt = insp(newPanelHostElement) || 0;
  91.  
  92. newPanelCnt.data = {
  93. "style": "STYLE_DEFAULT",
  94. "size": "SIZE_DEFAULT",
  95. "isDisabled": false,
  96. "serviceEndpoint": {
  97. },
  98. "icon": {
  99. },
  100. "tooltip": "My ToolTip",
  101. "trackingParams": "",
  102. "accessibilityData": {
  103. "accessibilityData": {
  104. "label": "My ToolTip"
  105. }
  106. }
  107. };
  108.  
  109. newPanelHostElement.classList.add('style-scope', 'ytd-watch-flexy');
  110. // $0.appendChild(newPanel);
  111.  
  112. // window.ss3 = newPanel
  113. // console.log(HTMLElement.prototype.querySelector.call(newPanel,'tp-yt-paper-tooltip'))
  114. return newPanelHostElement;
  115.  
  116. }
  117.  
  118.  
  119.  
  120. const formatDateFn = (d) => {
  121.  
  122. let y = d.getFullYear()
  123. let m = d.getMonth() + 1
  124. let date = d.getDate()
  125.  
  126. let sy = y < 1000 ? (`0000${y}`).slice(-4) : '' + y
  127.  
  128. let sm = m < 10 ? '0' + m : '' + m
  129. let sd = date < 10 ? '0' + date : '' + date
  130.  
  131. return `${sy}.${sm}.${sd}`
  132.  
  133. }
  134.  
  135. const formatTimeFn = (d) => {
  136.  
  137. let h = d.getHours()
  138. let m = d.getMinutes()
  139. let s = d.getSeconds()
  140.  
  141. const k = this.dayBack
  142.  
  143. if (k) h += 24
  144.  
  145. let sh = h < 10 ? '0' + h : '' + h
  146. let sm = m < 10 ? '0' + m : '' + m
  147.  
  148. let ss = s < 10 ? '0' + s : '' + s;
  149.  
  150.  
  151. return `${sh}:${sm}:${ss}`
  152.  
  153. }
  154.  
  155.  
  156. function onYtpTimeDisplayHover(evt) {
  157.  
  158. const promiseReady = new Promise((resolve) => {
  159.  
  160. if (document.querySelector('#live-time-display-dom')) {
  161. resolve()
  162. return;
  163. }
  164.  
  165. // evt.target.style.position='relative';
  166. let p = createElement();
  167. p.id = 'live-time-display-dom';
  168. p.setAttribute('hidden', '');
  169. let events = {}
  170. let controller = null;
  171. let running = false;
  172. const loop = async (o) => {
  173. const { formatDates, video } = o;
  174.  
  175. if (!formatDates || !video) return;
  176.  
  177. while (true) {
  178.  
  179.  
  180. if (!running) return;
  181.  
  182.  
  183.  
  184.  
  185. let k = formatDates.broadcastBeginAt;
  186. if (k) {
  187. let dt = new Date(k);
  188. dt.setTime(dt.getTime() + video.currentTime * 1000);
  189.  
  190. let t = formatDateFn(dt) + ' ' + formatTimeFn(dt);
  191. if (controller.data.tooltip !== t) {
  192.  
  193. controller.data.tooltip = t;
  194. controller.data = Object.assign({}, controller.data);
  195. }
  196. }
  197.  
  198.  
  199.  
  200. // controller.data.tooltip=Date.now()+"";
  201. // controller.data = Object.assign({}, controller.data);
  202.  
  203. if (!running) return;
  204. await new Promise(requestAnimationFrame);
  205.  
  206.  
  207. }
  208. }
  209. let pres = {
  210. 'mouseenter': function (evt) {
  211.  
  212. if (!controller) return -1;
  213. running = true;
  214. const formatDates = getFormatDates();
  215. const video = document.querySelector('#movie_player video');
  216.  
  217.  
  218. // controller.data.tooltip=Date.now()+"";
  219. // controller.data = Object.assign({}, controller.data);
  220.  
  221. if (formatDates && video && formatDates.broadcastBeginAt) {
  222. loop({ formatDates, video });
  223.  
  224. } else {
  225. if (controller.data.tooltip) {
  226.  
  227. controller.data.tooltip = '';
  228. controller.data = Object.assign({}, controller.data);
  229. }
  230.  
  231. return -1;
  232. }
  233.  
  234.  
  235.  
  236. }, 'mouseleave': function () {
  237.  
  238.  
  239.  
  240. if (!controller) return -1;
  241. if (!running) return -1;
  242. running = false;
  243.  
  244.  
  245.  
  246. }
  247. };
  248.  
  249. const eventHandler = function (evt) {
  250. const res = pres[evt.type].apply(this, arguments);
  251. if (res === -1) return;
  252. return events[evt.type].apply(this, arguments);
  253. };
  254.  
  255. p.addEventListener = function (type, fn, opts) {
  256. if (type === 'mouseenter' || type === 'mouseleave') {
  257. if (controller === null) {
  258. let cnt = insp(this);
  259. if (cnt.data !== null) {
  260. controller = cnt;
  261. if (!('data' in evt.target)) evt.target.data = controller.data;
  262. }
  263. }
  264. events[type] = fn;
  265. evt.target.addEventListener(type, eventHandler, opts);
  266. }
  267. // console.log(155, type, fn, opts)
  268. }
  269. // p.style.position='relative';
  270. p.style.position = 'absolute'
  271. evt.target.insertBefore(p, evt.target.firstChild);
  272.  
  273. Promise.resolve().then(() => {
  274. if (!events.mouseenter || !events.mouseleave) {
  275. return p.remove();
  276. }
  277. HTMLElement.prototype.querySelector.call(p, 'yt-button-shape').remove();
  278.  
  279. let tooltip = HTMLElement.prototype.querySelector.call(p, 'tp-yt-paper-tooltip');
  280. if (!tooltip) return p.remove();
  281.  
  282. const rect = evt.target.getBoundingClientRect()
  283. p.style.width = rect.width + 'px';
  284. p.style.height = rect.height + 'px';
  285.  
  286. let tooltipCnt = insp(tooltip);
  287. if (tooltip && tooltipCnt.position === 'bottom') {
  288. tooltipCnt.position = 'top';
  289. }
  290.  
  291. tooltip.removeAttribute('fit-to-visible-bounds');
  292. tooltip.setAttribute('offset', '0');
  293. p.removeAttribute('hidden')
  294.  
  295. if (evt.target.matches(':hover')) {
  296. eventHandler.call(evt.target, { type: 'mouseenter', target: evt.target });
  297. }
  298.  
  299. }).then(resolve);
  300.  
  301. });
  302.  
  303. promiseReady.then(() => {
  304. let dom = document.querySelector('#live-time-display-dom');
  305. if (!dom) return;
  306. // evt.target.data.tooltip=
  307. })
  308.  
  309. }
  310.  
  311. document.addEventListener('animationstart', (evt) => {
  312.  
  313. if (evt.animationName === 'ytpTimeDisplayHover') onYtpTimeDisplayHover(evt);
  314.  
  315. }, capturePassive);
  316.  
  317. const styleOpts = {
  318. id: 'vEXik',
  319. textContent: `
  320.  
  321. @keyframes ytpTimeDisplayHover {
  322. 0% {
  323. background-position-x: 3px;
  324. }
  325. 100% {
  326. background-position-x: 4px;
  327. }
  328. }
  329. ytd-watch-flexy #movie_player .ytp-time-display:hover {
  330. animation: ytpTimeDisplayHover 1ms linear 120ms 1 normal forwards;
  331. }
  332. #live-time-display-dom {
  333. position: absolute;
  334. pointer-events: none;
  335. }
  336. #live-time-display-dom yt-button-shape {
  337. display: none;
  338. }
  339. @supports (-webkit-text-stroke:0.5px #000) {
  340. #live-time-display-dom tp-yt-paper-tooltip #tooltip {
  341. background: transparent;
  342. color: #fff;
  343. -webkit-text-stroke: 0.5px #000;
  344. font-weight: 700;
  345. font-size: 12pt;
  346. }
  347. }
  348.  
  349. `
  350.  
  351. };
  352.  
  353. function onReady() {
  354.  
  355. document.head.appendChild(Object.assign(document.createElement('style'), styleOpts));
  356.  
  357. }
  358.  
  359. Promise.resolve().then(() => {
  360.  
  361. if (document.readyState !== 'loading') {
  362. onReady();
  363. } else {
  364. window.addEventListener("DOMContentLoaded", onReady, false);
  365. }
  366.  
  367. });
  368.  
  369. })({ Promise, requestAnimationFrame });

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址