| 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 | */
|
| 18 | function 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 | */
|
| 50 | function 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 | */
|
| 87 | async 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 | */
|
| 114 | async 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 | */
|
| 140 | async 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 | */
|
| 160 | let checkedUrl = null;
|
| 161 | function 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 |
|
| 191 | const observer = new MutationObserver(observerHandler);
|
| 192 | observer.observe(document, {
|
| 193 | attributes: false,
|
| 194 | childList: true,
|
| 195 | characterData: false,
|
| 196 | subtree: true,
|
| 197 | });
|