Skip to content

Instantly share code, notes, and snippets.

@serg06
Last active August 6, 2024 05:06
Show Gist options
  • Save serg06/e9871166652c1cf30556654314baf622 to your computer and use it in GitHub Desktop.
Save serg06/e9871166652c1cf30556654314baf622 to your computer and use it in GitHub Desktop.
Greasemonkey script for displaying the real time on Twitch VOD timestamps
// ==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