Forked from Alex/Twitch clip datetime userscript

Last active 1707301996

A userscript for displaying the actual date & time (relative to local time) of when a Twitch clip was created.

Revision 08331d7c8fa7ff8675fbceedea9c4aec6f005cf2

README.md Raw

Twitch clip datetime userscript

A userscript for displaying the actual date & time (relative to local time) of when a Twitch clip (and video) was created.

FYI: It only works on URLs that start with https://clips.twitch.tv/.
This script does not work with URLs that are on the Twitch "channel pages" (https://www.twitch.tv/CHANNEL_NAME_HERE/clip/...).
This has been added as of v0.5.0.

"Under the hood" the script uses Date.toLocaleString() to format the date. The format of the date & time may differ from the screenshots below.

Requirements:

  • Something like the Violentmonkey extension installed for your browser.

Installation:

  1. Install a userscript extension (such as Violentmonkey).
  2. Click on this link when Violentmonkey is installed, and it will prompt you to install the userscript.

Support / Requests

If you have any requests or suggestions, feel free to join my Discord server for all my projects.

Changelog

  • v0.6.0 - 2022-01-03
    • Added support for Twitch videos (VODs, highlights)
    • Switched API to use the new Twitch API (Helix) via an "API proxy" I host using Cloudflare Workers.
    • The observer should no longer disconnect after the first timestamp, allowing browsing to other videos/clips to work.
  • v0.5.1 - 2021-12-30
    • Minor bug where it tried to fetch the date & time of a clip when watching highlights/VODs.
      • At some point I'll update the script to support VODs/highlights, but for now I'm sticking to clips.
  • v0.5.0 - 2021-12-30
    • Script now supports clips on Twitch channel pages (e.g. https://www.twitch.tv/$channelName/clip/$clipSlug)
      • The @match rule will say https://www.twitch.tv/* to support navigating from the channel page to the clip page, otherwise script wouldn't load on regular Twitch browsing.
  • v0.4.1 - 2021-08-29
    • Make script compatible with Tampermonkey again.
  • v0.4.0 - 2021-08-29
    • Date/time placement was adjusted due to changes in the page on Twitch's end. See screenshots.
    • Previously I used Tampermonkey, but later switched to Violentmonkey. During my testing, I tested with Violentmonkey and it worked fine. However, if you're currently using Tampermonkey, v0.4.0 does not work. I'm unsure as to why, but I'll try to look into it.
    • Fixed in v0.4.1

Screenshots:

As of v0.4.0, the timestamp shows up a bit different compared to earlier versions.

Without chat (beginning of clip)

Screenshot of userscript in action, before chat history loads

With chat

Screenshot of userscript in action, with chat history loaded

Main Twitch website

When browsing Twitch channel clips on the main Twitch website.
Only v0.5.0 (December 30th, 2021) and newer

Screenshot of userscript on main Twitch website'

As of v0.6.0 (January 3rd, 2022), the script also works with Twitch videos (VODs, highlights).

twitch-clips-datetime.user.js Raw
1// ==UserScript==
2// @name Twitch Clips - Show date & time
3// @version 0.6.0
4// @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"
5// @author Decicus
6// @updateURL https://gist.github.com/Decicus/ec4745e680e06cfff5b1fa0a53fcff72/raw/twitch-clips-datetime.user.js
7// @downloadURL https://gist.github.com/Decicus/ec4745e680e06cfff5b1fa0a53fcff72/raw/twitch-clips-datetime.user.js
8// @homepageURL https://gist.github.com/Decicus/ec4745e680e06cfff5b1fa0a53fcff72
9// @icon https://i.alex.lol/2021-08-29_PmO4zo.png
10// @match https://clips.twitch.tv/*
11// @match https://www.twitch.tv/*
12// @license MIT
13// ==/UserScript==
14
15/**
16 * Insert timestamp for the `clips.twitch.tv` website.
17 */
18function clipsSubdomain(createdAt, dateAndTime)
19{
20 const box = document.querySelector('.clips-sidebar-info');
21 const layoutClass = box.classList[0];
22
23 const textElement = document.querySelector('span[class*="CoreText"]');
24 const textClass = textElement.classList[0];
25
26 let element = document.querySelector('#twitch-datetime-script');
27 let newElement = false;
28 if (!element) {
29 newElement = true;
30 element = document.createElement('div');
31 element.className = layoutClass;
32 element.setAttribute('style', 'text-align: center; margin-top: 1em;');
33 element.setAttribute('id', 'twitch-datetime-script');
34 }
35
36 element.innerHTML = `<span class="${textClass}" title="${createdAt}"><strong>Clip created:</strong> ${dateAndTime}</span>`;
37
38 if (!newElement) {
39 return;
40 }
41
42 box.insertAdjacentElement('afterbegin', element);
43}
44
45/**
46 * Insert timestamp for the `www.twitch.tv` (main) website.
47 * E.g. https://www.twitch.tv/$channelName/clip/$clipSlug
48 * Or: https://www.twitch.tv/videos/$videoId
49 */
50function insertMainWebsite(createdAt, dateAndTime, isVideo)
51{
52 const timestampBar = document.querySelector('.timestamp-metadata__bar');
53 const parent = timestampBar.parentElement;
54
55 // Use for text styling
56 const textElement = document.querySelector('p[class*="CoreText"]');
57 const textClass = textElement.classList[0];
58
59 let element = document.querySelector('#twitch-datetime-script');
60 let newElement = false;
61 if (!element) {
62 element = document.createElement('p');
63 element.className = textClass;
64 element.setAttribute('style', 'margin-left: 0.25em;');
65 element.setAttribute('id', 'twitch-datetime-script');
66
67 newElement = true;
68 }
69
70 element.innerHTML = `- ${isVideo ? 'Video' : 'Clip'} created: <strong>${dateAndTime}</strong>`;
71 element.setAttribute('title', createdAt);
72
73 if (!newElement) {
74 return;
75 }
76
77 parent.insertAdjacentElement('beforeend', element);
78}
79
80/**
81 * Get the timestamp of a clip or video.
82 *
83 * @param {string} id The id of the clip or video.
84 * @param {string} type Type: `clip` or `video`
85 * @returns {Promise<{createdAt: string, dateAndTime: string}>}
86 */
87async function getTimestamp(id, type)
88{
89 if (!type) {
90 type = 'clip';
91 }
92
93 const response = await fetch(`https://twitch-api-proxy.cactus.workers.dev/timestamps/${type}?id=${id}`);
94
95 const data = await response.json();
96
97 if (!data.created_at) {
98 return;
99 }
100
101 const createdAt = data.created_at;
102 const created = new Date(createdAt);
103 const dateAndTime = created.toLocaleString();
104
105 return {createdAt, dateAndTime};
106}
107
108/**
109 * Fetch clip information via getTimestamp() and insert accordingly.
110 *
111 * @param {URL} url
112 * @returns {void}
113 */
114async function fetchClip(url) {
115 const pathFragments = url.pathname.split('/');
116 const clipSlug = pathFragments[pathFragments.length - 1];
117
118 const slug = clipSlug.match(/([A-z0-9-_]+)/m)[1];
119
120 if (!slug) {
121 return;
122 }
123
124 const { createdAt, dateAndTime } = await getTimestamp(slug, 'clip');
125
126 if (url.hostname === 'clips.twitch.tv') {
127 clipsSubdomain(createdAt, dateAndTime);
128 return;
129 }
130
131 insertMainWebsite(createdAt, dateAndTime, false);
132}
133
134/**
135 * Fetch video information via getTimestamp() and insert accordingly.
136 *
137 * @param {URL} url
138 * @returns {void}
139 */
140async function fetchVideo(url)
141{
142 const pathFragments = url.pathname.split('/');
143 const videoFragment = pathFragments[pathFragments.length - 1];
144
145 const videoId = videoFragment.match(/(^\d+$)/m)[1];
146
147 if (!videoId) {
148 return;
149 }
150
151 const { createdAt, dateAndTime } = await getTimestamp(videoId, 'video');
152 insertMainWebsite(createdAt, dateAndTime, true);
153}
154
155/**
156 * Observe the DOM until we find the element we're interested in.
157 * Once complete, disconnect the observer and call the `fetchClip()` function which actually inserts the
158 * timestamp into the DOM.
159 */
160let checkedUrl = null;
161function observerHandler(mutations, observer)
162{
163 // clips.twitch.tv
164 const clipsInfo = document.querySelector('.clips-chat-info span[class*="CoreText"]');
165
166 // www.twitch.tv
167 const timestampBar = document.querySelector('.timestamp-metadata__bar');
168
169 if (!clipsInfo && !timestampBar) {
170 return;
171 }
172
173 const urlString = window.location.href;
174 if (urlString === checkedUrl) {
175 return;
176 }
177 checkedUrl = urlString;
178
179 console.log('Clips page', clipsInfo !== null);
180 console.log('Main website', timestampBar !== null);
181
182 const url = new URL(urlString);
183 if (url.hostname === 'www.twitch.tv' && url.pathname.includes('/videos/')) {
184 fetchVideo(url);
185 }
186 else {
187 fetchClip(url);
188 }
189}
190
191const observer = new MutationObserver(observerHandler);
192observer.observe(document, {
193 attributes: false,
194 childList: true,
195 characterData: false,
196 subtree: true,
197});