Last active
August 6, 2024 05:06
-
-
Save serg06/e9871166652c1cf30556654314baf622 to your computer and use it in GitHub Desktop.
Greasemonkey script for displaying the real time on Twitch VOD timestamps
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name New Userscript | |
// @namespace http://tampermonkey.net/ | |
// @version 2024-08-06 | |
// @description try to take over the world! | |
// @author You | |
// @match https://www.twitch.tv/videos/* | |
// @icon https://www.google.com/s2/favicons?sz=64&domain=twitch.tv | |
// @grant none | |
// ==/UserScript== | |
function getVodId() { | |
return location.pathname.split('/').at(-1); | |
} | |
async function sleep(ms) { | |
return new Promise((resolve) => setTimeout(resolve, ms)); | |
} | |
async function findParent() { | |
console.log('Finding timebar node'); | |
for (let i = 0; i < 10; i++) { | |
let targetChild = document.querySelector('.vod-seekbar-time-labels'); | |
if (targetChild) { | |
console.log('Found timebar node!'); | |
return targetChild.parentNode; | |
} | |
await sleep(1000); | |
} | |
console.log('Failed to find timebar node.'); | |
} | |
async function getNielsenContentMetadata(vod_id) { | |
const resp = await fetch('https://gql.twitch.tv/gql', { | |
method: 'POST', | |
body: JSON.stringify([{ | |
extensions: { | |
persistedQuery: { | |
sha256Hash: '2dbf505ee929438369e68e72319d1106bb3c142e295332fac157c90638968586', | |
version: 1 | |
} | |
}, | |
operationName: 'NielsenContentMetadata', | |
variables: { | |
collectionID: '', | |
isCollectionContent: false, | |
isLiveContent: false, | |
isVODContent: true, | |
login: '', | |
vodID: `${vod_id}` | |
} | |
}]), | |
headers: { | |
'Client-Id': 'kimne78kx3ncx6brgo4mv6wki5h1ko' | |
} | |
}); | |
const result = await resp.json(); | |
return result[0].data; | |
} | |
async function getVodStart(vod_id) { | |
const metadata = await getNielsenContentMetadata(vod_id); | |
const result = metadata.video.createdAt; | |
return new Date(result); | |
} | |
const VOD_START_CACHE = {}; | |
async function getVodStartCached(vod_id) { | |
if (!(vod_id in VOD_START_CACHE)) { | |
VOD_START_CACHE[vod_id] = await getVodStart(vod_id); | |
} | |
return VOD_START_CACHE[vod_id]; | |
} | |
function parseTimestamp(timestamp) { | |
const match = timestamp.match(/((?<h>\d+):)?((?<m>\d+):)?(?<s>\d+)$/); | |
if (!match) { | |
return undefined; | |
} | |
const {h, m, s} = match.groups; | |
return { | |
h: parseInt(h, 10), | |
m: parseInt(m, 10), | |
s: parseInt(s, 10) | |
} | |
} | |
function timestampToMs(parsed_timestamp) { | |
const {h, m, s} = parsed_timestamp; | |
const seconds = s + m * 60 + h * 60 * 60; | |
return seconds * 1000; | |
} | |
function formatDate(date) { | |
return date.toLocaleDateString('en-us', { | |
hour: 'numeric', | |
minute: 'numeric', | |
second: 'numeric', | |
day: 'numeric', | |
month: 'long', | |
timeZoneName: 'short' | |
}) | |
} | |
async function main() { | |
const parent = await findParent(); | |
if (!parent) { | |
return; | |
} | |
const vod_id = await getVodId(); | |
const vod_start = await getVodStart(vod_id); | |
const observer = new MutationObserver((mutationList, observer) => { | |
// console.log(`Mutation of types: ${mutationList.map(x => x.type)}`); | |
const timeElement = parent.querySelector('div.vod-seekbar-preview-overlay__wrapper p'); | |
if (!timeElement) { | |
return; | |
} | |
const timestamp = timeElement.innerText; | |
if (timestamp.includes('(')) { | |
// Already formatted | |
return; | |
} | |
const parsed_timestamp = parseTimestamp(timestamp); | |
if (!parsed_timestamp) { | |
// Weird, no data found | |
return; | |
} | |
const time = new Date(vod_start.getTime() + timestampToMs(parsed_timestamp)); | |
const updated = `${timeElement.innerText} (${formatDate(time)})`; | |
// console.log(`Changing timestamp from ${timestamp} to ${updated}`); | |
timeElement.innerText = updated; | |
}); | |
// TODO: Make this more efficient; when the element is created, give it its own observer. | |
observer.observe(parent, { | |
attributes: true, | |
childList: true, | |
subtree: true | |
}); | |
// observer.disconnect(); | |
} | |
(function() { | |
'use strict'; | |
main(); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment