// ==UserScript==
// @name Twitch Clips - Show date & time
// @version 0.6.0
// @description Displays the actual date & time of when a clip (or video/VOD/highlight) was created, instead of the useless "xxx days/months/weeks ago"
// @author Decicus
// @updateURL https://gist.github.com/Decicus/ec4745e680e06cfff5b1fa0a53fcff72/raw/twitch-clips-datetime.user.js
// @downloadURL https://gist.github.com/Decicus/ec4745e680e06cfff5b1fa0a53fcff72/raw/twitch-clips-datetime.user.js
// @homepageURL https://gist.github.com/Decicus/ec4745e680e06cfff5b1fa0a53fcff72
// @icon https://i.alex.lol/2021-08-29_PmO4zo.png
// @match https://clips.twitch.tv/*
// @match https://www.twitch.tv/*
// @license MIT
// ==/UserScript==
/**
* Insert timestamp for the `clips.twitch.tv` website.
*/
function clipsSubdomain(createdAt, dateAndTime)
{
const box = document.querySelector('.clips-sidebar-info');
const layoutClass = box.classList[0];
const textElement = document.querySelector('span[class*="CoreText"]');
const textClass = textElement.classList[0];
let element = document.querySelector('#twitch-datetime-script');
let newElement = false;
if (!element) {
newElement = true;
element = document.createElement('div');
element.className = layoutClass;
element.setAttribute('style', 'text-align: center; margin-top: 1em;');
element.setAttribute('id', 'twitch-datetime-script');
}
element.innerHTML = `Clip created: ${dateAndTime}`;
if (!newElement) {
return;
}
box.insertAdjacentElement('afterbegin', element);
}
/**
* Insert timestamp for the `www.twitch.tv` (main) website.
* E.g. https://www.twitch.tv/$channelName/clip/$clipSlug
* Or: https://www.twitch.tv/videos/$videoId
*/
function insertMainWebsite(createdAt, dateAndTime, isVideo)
{
const timestampBar = document.querySelector('.timestamp-metadata__bar');
const parent = timestampBar.parentElement;
// Use for text styling
const textElement = document.querySelector('p[class*="CoreText"]');
const textClass = textElement.classList[0];
let element = document.querySelector('#twitch-datetime-script');
let newElement = false;
if (!element) {
element = document.createElement('p');
element.className = textClass;
element.setAttribute('style', 'margin-left: 0.25em;');
element.setAttribute('id', 'twitch-datetime-script');
newElement = true;
}
element.innerHTML = `- ${isVideo ? 'Video' : 'Clip'} created: ${dateAndTime}`;
element.setAttribute('title', createdAt);
if (!newElement) {
return;
}
parent.insertAdjacentElement('beforeend', element);
}
/**
* Get the timestamp of a clip or video.
*
* @param {string} id The id of the clip or video.
* @param {string} type Type: `clip` or `video`
* @returns {Promise<{createdAt: string, dateAndTime: string}>}
*/
async function getTimestamp(id, type)
{
if (!type) {
type = 'clip';
}
const response = await fetch(`https://twitch-api-proxy.cactus.workers.dev/timestamps/${type}?id=${id}`);
const data = await response.json();
if (!data.created_at) {
return;
}
const createdAt = data.created_at;
const created = new Date(createdAt);
const dateAndTime = created.toLocaleString();
return {createdAt, dateAndTime};
}
/**
* Fetch clip information via getTimestamp() and insert accordingly.
*
* @param {URL} url
* @returns {void}
*/
async function fetchClip(url) {
const pathFragments = url.pathname.split('/');
const clipSlug = pathFragments[pathFragments.length - 1];
const slug = clipSlug.match(/([A-z0-9-_]+)/m)[1];
if (!slug) {
return;
}
const { createdAt, dateAndTime } = await getTimestamp(slug, 'clip');
if (url.hostname === 'clips.twitch.tv') {
clipsSubdomain(createdAt, dateAndTime);
return;
}
insertMainWebsite(createdAt, dateAndTime, false);
}
/**
* Fetch video information via getTimestamp() and insert accordingly.
*
* @param {URL} url
* @returns {void}
*/
async function fetchVideo(url)
{
const pathFragments = url.pathname.split('/');
const videoFragment = pathFragments[pathFragments.length - 1];
const videoId = videoFragment.match(/(^\d+$)/m)[1];
if (!videoId) {
return;
}
const { createdAt, dateAndTime } = await getTimestamp(videoId, 'video');
insertMainWebsite(createdAt, dateAndTime, true);
}
/**
* Observe the DOM until we find the element we're interested in.
* Once complete, disconnect the observer and call the `fetchClip()` function which actually inserts the
* timestamp into the DOM.
*/
let checkedUrl = null;
function observerHandler(mutations, observer)
{
// clips.twitch.tv
const clipsInfo = document.querySelector('.clips-chat-info span[class*="CoreText"]');
// www.twitch.tv
const timestampBar = document.querySelector('.timestamp-metadata__bar');
if (!clipsInfo && !timestampBar) {
return;
}
const urlString = window.location.href;
if (urlString === checkedUrl) {
return;
}
checkedUrl = urlString;
console.log('Clips page', clipsInfo !== null);
console.log('Main website', timestampBar !== null);
const url = new URL(urlString);
if (url.hostname === 'www.twitch.tv' && url.pathname.includes('/videos/')) {
fetchVideo(url);
}
else {
fetchClip(url);
}
}
const observer = new MutationObserver(observerHandler);
observer.observe(document, {
attributes: false,
childList: true,
characterData: false,
subtree: true,
});