// ==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.twoColumnWatchNextResults.results.results.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 });