Make a tooltip to show the actual date and time for livestream
// ==UserScript== // @name YouTube Live DateTime Tooltip // @namespace UserScript // @match https://www.youtube.com/* // @version 0.1.5 // @license MIT // @author CY Fung // @run-at document-start // @grant none // @unwrap // @inject-into page // @description Make a tooltip to show the actual date and time for livestream // ==/UserScript== ((__CONTEXT__) => { const { Promise, requestAnimationFrame } = __CONTEXT__; const isPassiveArgSupport = (typeof IntersectionObserver === 'function'); const bubblePassive = isPassiveArgSupport ? { capture: false, passive: true } : false; const capturePassive = isPassiveArgSupport ? { capture: true, passive: true } : true; const insp = o => o ? (o.polymerController || o.inst || o || 0) : (o || 0); let pageFetchedDataLocal = null; document.addEventListener('yt-page-data-fetched', (evt) => { pageFetchedDataLocal = evt.detail; }, bubblePassive); function getFormatDates() { if (!pageFetchedDataLocal) return null; const formatDates = {} try { formatDates.publishDate = pageFetchedDataLocal.pageData.playerResponse.microformat.playerMicroformatRenderer.publishDate } catch (e) { } // 2022-12-30 try { formatDates.uploadDate = pageFetchedDataLocal.pageData.playerResponse.microformat.playerMicroformatRenderer.uploadDate } catch (e) { } // 2022-12-30 try { formatDates.publishDate2 = pageFetchedDataLocal.pageData.response.contents.twoColumnWatchNextR###lts.r###lts.r###lts.contents[0].videoPrimaryInfoRenderer.dateText.simpleText } catch (e) { } // 2022/12/31 if (typeof formatDates.publishDate2 === 'string' && formatDates.publishDate2 !== formatDates.publishDate) { formatDates.publishDate = formatDates.publishDate2 formatDates.uploadDate = null } try { formatDates.broadcastBeginAt = pageFetchedDataLocal.pageData.playerResponse.microformat.playerMicroformatRenderer.liveBroadcastDetails.startTimestamp } catch (e) { } try { formatDates.broadcastEndAt = pageFetchedDataLocal.pageData.playerResponse.microformat.playerMicroformatRenderer.liveBroadcastDetails.endTimestamp } catch (e) { } try { formatDates.isLiveNow = pageFetchedDataLocal.pageData.playerResponse.microformat.playerMicroformatRenderer.liveBroadcastDetails.isLiveNow } catch (e) { } return formatDates; } function createElement() { /** @type {HTMLElement} */ const ytdWatchFlexyElm = document.querySelector('ytd-watch-flexy'); if (!ytdWatchFlexyElm) return; const ytdWatchFlexyCnt = insp(ytdWatchFlexyElm); let newPanel = ytdWatchFlexyCnt.createComponent_({ "component": "ytd-button-renderer", "params": { buttonTooltipPosition: "top", systemIcons: false, modern: true, forceIconButton: false, } }, "ytd-engagement-panel-section-list-renderer", true) || 0; const newPanelHostElement = newPanel.hostElement || newPanel || 0; const newPanelCnt = insp(newPanelHostElement) || 0; newPanelCnt.data = { "style": "STYLE_DEFAULT", "size": "SIZE_DEFAULT", "isDisabled": false, "serviceEndpoint": { }, "icon": { }, "tooltip": "My ToolTip", "trackingParams": "", "accessibilityData": { "accessibilityData": { "label": "My ToolTip" } } }; newPanelHostElement.classList.add('style-scope', 'ytd-watch-flexy'); // $0.appendChild(newPanel); // window.ss3 = newPanel // console.log(HTMLElement.prototype.querySelector.call(newPanel,'tp-yt-paper-tooltip')) return newPanelHostElement; } const formatDateFn = (d) => { let y = d.getFullYear() let m = d.getMonth() + 1 let date = d.getDate() let sy = y < 1000 ? (`0000${y}`).slice(-4) : '' + y let sm = m < 10 ? '0' + m : '' + m let sd = date < 10 ? '0' + date : '' + date return `${sy}.${sm}.${sd}` } const formatTimeFn = (d) => { let h = d.getHours() let m = d.getMinutes() let s = d.getSeconds() const k = this.dayBack if (k) h += 24 let sh = h < 10 ? '0' + h : '' + h let sm = m < 10 ? '0' + m : '' + m let ss = s < 10 ? '0' + s : '' + s; return `${sh}:${sm}:${ss}` } function onYtpTimeDisplayHover(evt) { const promiseReady = new Promise((resolve) => { if (document.querySelector('#live-time-display-dom')) { resolve() return; } // evt.target.style.position='relative'; let p = createElement(); p.id = 'live-time-display-dom'; p.setAttribute('hidden', ''); let events = {} let controller = null; let running = false; const loop = async (o) => { const { formatDates, video } = o; if (!formatDates || !video) return; while (true) { if (!running) return; let k = formatDates.broadcastBeginAt; if (k) { let dt = new Date(k); dt.setTime(dt.getTime() + video.currentTime * 1000); let t = formatDateFn(dt) + ' ' + formatTimeFn(dt); if (controller.data.tooltip !== t) { controller.data.tooltip = t; controller.data = Object.assign({}, controller.data); } } // controller.data.tooltip=Date.now()+""; // controller.data = Object.assign({}, controller.data); if (!running) return; await new Promise(requestAnimationFrame); } } let pres = { 'mouseenter': function (evt) { if (!controller) return -1; running = true; const formatDates = getFormatDates(); const video = document.querySelector('#movie_player video'); // controller.data.tooltip=Date.now()+""; // controller.data = Object.assign({}, controller.data); if (formatDates && video && formatDates.broadcastBeginAt) { loop({ formatDates, video }); } else { if (controller.data.tooltip) { controller.data.tooltip = ''; controller.data = Object.assign({}, controller.data); } return -1; } }, 'mouseleave': function () { if (!controller) return -1; if (!running) return -1; running = false; } }; const eventHandler = function (evt) { const res = pres[evt.type].apply(this, arguments); if (res === -1) return; return events[evt.type].apply(this, arguments); }; p.addEventListener = function (type, fn, opts) { if (type === 'mouseenter' || type === 'mouseleave') { if (controller === null) { let cnt = insp(this); if (cnt.data !== null) { controller = cnt; if (!('data' in evt.target)) evt.target.data = controller.data; } } events[type] = fn; evt.target.addEventListener(type, eventHandler, opts); } // console.log(155, type, fn, opts) } // p.style.position='relative'; p.style.position = 'absolute' evt.target.insertBefore(p, evt.target.firstChild); Promise.resolve().then(() => { if (!events.mouseenter || !events.mouseleave) { return p.remove(); } HTMLElement.prototype.querySelector.call(p, 'yt-button-shape').remove(); let tooltip = HTMLElement.prototype.querySelector.call(p, 'tp-yt-paper-tooltip'); if (!tooltip) return p.remove(); const rect = evt.target.getBoundingClientRect() p.style.width = rect.width + 'px'; p.style.height = rect.height + 'px'; let tooltipCnt = insp(tooltip); if (tooltip && tooltipCnt.position === 'bottom') { tooltipCnt.position = 'top'; } tooltip.removeAttribute('fit-to-visible-bounds'); tooltip.setAttribute('offset', '0'); p.removeAttribute('hidden') if (evt.target.matches(':hover')) { eventHandler.call(evt.target, { type: 'mouseenter', target: evt.target }); } }).then(resolve); }); promiseReady.then(() => { let dom = document.querySelector('#live-time-display-dom'); if (!dom) return; // evt.target.data.tooltip= }) } document.addEventListener('animationstart', (evt) => { if (evt.animationName === 'ytpTimeDisplayHover') onYtpTimeDisplayHover(evt); }, capturePassive); const styleOpts = { id: 'vEXik', textContent: ` @keyframes ytpTimeDisplayHover { 0% { background-position-x: 3px; } 100% { background-position-x: 4px; } } ytd-watch-flexy #movie_player .ytp-time-display:hover { animation: ytpTimeDisplayHover 1ms linear 120ms 1 normal forwards; } #live-time-display-dom { position: absolute; pointer-events: none; } #live-time-display-dom yt-button-shape { display: none; } @supports (-webkit-text-stroke:0.5px #000) { #live-time-display-dom tp-yt-paper-tooltip #tooltip { background: transparent; color: #fff; -webkit-text-stroke: 0.5px #000; font-weight: 700; font-size: 12pt; } } ` }; function onReady() { document.head.appendChild(Object.assign(document.createElement('style'), styleOpts)); } Promise.resolve().then(() => { if (document.readyState !== 'loading') { onReady(); } else { window.addEventListener("DOMContentLoaded", onReady, false); } }); })({ Promise, requestAnimationFrame });