// ==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, });