Skip to content

Instantly share code, notes, and snippets.

@mchangrh
Last active January 9, 2023 02:57
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save mchangrh/9507604353e37b6abc2f7f6b3c6e1338 to your computer and use it in GitHub Desktop.
Save mchangrh/9507604353e37b6abc2f7f6b3c6e1338 to your computer and use it in GitHub Desktop.
sb.ltn.fi userscripts

this gist has been deprecated, please go to https://github.com/mchangrh/uscripts instead

Userscripts for sb.ltn.fi

sbltnfi-clickable-starttime-fork.user.js

Fork of NanoByte's Script with more aggressive videoID searching

sbltnfi-discord-badge.user.js

Add Discord badge to any users that are registered with sb-slash on sb.ltn.fi

sbltnfi-imprecise-times.user.js

Removes trailing zeros from start, end and length times

sbltnfi-it-videotitle.user.js

Fork of TheJzoli's Script using innerTube instead of public invidious instances

sbltnfi-oembed-videotitle.user.js

Fork of TheJzoli's Script using OEmbed API instead of public invidious instances

sbltnfi-preset-replace.user.js

Replaces or redirct all sb.ltn.fi links to be pre-loaded with filters

sbltnfi-preset-video-link.user.js

Fork of Deedit's Script with preset search parameters

sbltnfi-refresh.user.js

Force refresh a single segment

sbltnfi-requiredSegments.user.js

Adds a required segment link to all entries

yt-warn-reqseg.user.js

Adds a very big, red and annoying warning at the top of the screen with requiredSegment is present

sbltnfi-export-segments.user.js

Export sbltnfi segments into loadable URLs

yt-mstime.user.js

Add milliseconds to YT end time

yt-warn-postlive.user.js

Warn if video is post-live manifestless

yt-frames.user.js

Add frames to YT time

yt-moreseek.user.js

Add additional seeking options to YT

A/D for back/forth

  • Ctrl = 10s
  • Default = 1s
  • Shift = 0.1s
// ==UserScript==
// @name SponsorBlock clickable startTime (sb.ltn.fi) fork
// @namespace mchang-sb.ltn.fi.clickable.starttime
// @version 1.1.11
// @description Makes the startTime clickable
// @author michael mchang.name
// @match https://sb.ltn.fi/*
// @updateURL https://raw.githubusercontent.com/mchangrh/uscripts/main/sbltnfi/fork/sbltnfi-clickable-starttime-fork.user.js
// @downloadURL https://raw.githubusercontent.com/mchangrh/uscripts/main/sbltnfi/fork/sbltnfi-clickable-starttime-fork.user.js
// @require https://gist.github.com/mchangrh/9507604353e37b6abc2f7f6b3c6e1338/raw/stringToSec.js
// @grant none
// ==/UserScript==
const videoRegex = new RegExp(/(?:(?:video\/)|(?:videoid=))([0-9A-Za-z_-]{11})/);
const findVideoID = (str) => str.match(videoRegex)?.[1];
let pageVideoID = findVideoID(window.location.href);
let videoId;
function create() {
const table = document.querySelector("table.table");
const headers = [...table.querySelectorAll('thead th')].map(item =>
item.textContent.trim()
);
const startColumnIndex = headers.indexOf('Start');
const UUIDColumnIndex = headers.indexOf('UUID');
const videoIdColumnIndex = headers.indexOf('VideoID');
const rows = [...table.querySelectorAll('tbody tr')];
rows.forEach(row => {
if (!pageVideoID) {
videoId = row.children[videoIdColumnIndex].firstChild.textContent.trim()
} else {
videoId = pageVideoID
}
if (!videoId) return;
const UUID = row.children[UUIDColumnIndex].querySelector("textarea").textContent.trim()
const cellEl = row.children[startColumnIndex];
// check for existing children
if (cellEl.querySelector(".clickable-starttime")) return;
const content = cellEl.textContent.trim();
const link = document.createElement('a');
let startTimeSeconds = stringToSec(content, false)
// -2s to have time before skip
startTimeSeconds-=2;
link.textContent = content;
link.style.color = 'inherit';
link.classList.add("clickable-starttime");
const timeParam = startTimeSeconds > 0 ? `&t=${startTimeSeconds}s` : ""
link.href = `https://www.youtube.com/watch?v=${videoId}${timeParam}#requiredSegment=${UUID}`;
cellEl.innerHTML = '';
cellEl.appendChild(link);
});
}
function wrapElement(target, el) {
el.innerHTML = target.innerHTML;
target.innerHTML = el.innerHTML
}
(function (){
create()
document.addEventListener("newSegments", (event) => create());
})();
// ==UserScript==
// @name sb.ltn.fi discord
// @namespace mchang.name
// @version 1.1.2
// @description Indicates if a SB user is on discord
// @author michael mchang.name
// @match https://sb.ltn.fi/*
// @icon https://cdn.mchang.xyz/uscript/discord-badge.png
// @updateURL https://raw.githubusercontent.com/mchangrh/uscripts/main/sbltnfi/sbltnfi-discord-badge.user.js
// @downloadURL https://raw.githubusercontent.com/mchangrh/uscripts/main/sbltnfi/sbltnfi-discord-badge.user.js
// @connect mongo.mchang.xyz
// @grant GM_xmlhttpRequest
// ==/UserScript==
const AUTH = "tlB7XLNX3b33nbnx01hw"
function lookupUser (SBID, target) {
GM_xmlhttpRequest({
method: "GET",
url: `https://mongo.mchang.xyz/sb-vip/discord?auth=${AUTH}&sbID=${SBID}`,
timeout: 1000,
onload: (res) => addBadge(res.status === 200, target),
onerror: (res) => addBadge(false, target)
})
}
const discordBadge = document.createElement("img")
discordBadge.src = "https://cdn.mchang.xyz/uscript/discord-badge.svg"
discordBadge.style.height = "1em"
const spanElem = document.createElement("span");
spanElem.title = "This user is on Discord"
spanElem.classList = "badge bg-secondary ms-1"
spanElem.appendChild(discordBadge)
function addBadge (onDiscord, target) {
if (!onDiscord) return
const clone = spanElem.cloneNode(true)
if (!target) {
// scope to header
const header = document.querySelector("div.row.mt-2 > .col-auto > .list-group")
const username = header.children[0]
username.appendChild(clone)
} else {
target.after(clone)
}
}
(function () {
"use strict"
const pathname = new URL(document.URL).pathname
if (pathname.includes("/userid/")) {
const SBID = pathname.split("/")[2]
lookupUser(SBID)
} else {
document.querySelectorAll("a[href^='/userid/']").forEach(elem => {
const SBID = elem.href.split("/")[4]
lookupUser(SBID, elem)
})
}
})()
// ==UserScript==
// @name sb.ltn.fi export as #segments
// @namespace mchang.name
// @version 1.0.7
// @description Export sbltnfi segments into loadable URLs
// @author michael mchang.name
// @match https://sb.ltn.fi/*
// @updateURL https://raw.githubusercontent.com/mchangrh/uscripts/main/sbltnfi/sbltnfi-export-segments.user.js
// @downloadURL https://raw.githubusercontent.com/mchangrh/uscripts/main/sbltnfi/sbltnfi-export-segments.user.js
// @require https://gist.github.com/mchangrh/9507604353e37b6abc2f7f6b3c6e1338/raw/stringToSec.js
// @icon https://sb.ltn.fi/static/browser/logo.png
// @grant none
// ==/UserScript==
const videoRegex = new RegExp(/(?:(?:video\/)|(?:videoid=))([0-9A-Za-z_-]{11})/);
const findVideoID = (str) => str.match(videoRegex)?.[1];
let videoId = findVideoID(window.location.href);
function create() {
const table = document.querySelector("table.table");
const headers = [...table.querySelectorAll('thead th')].map(item =>
item.textContent.trim()
);
const startColumnIndex = headers.indexOf('Start');
const endColumnIndex = headers.indexOf('End');
const actionTypeColumnIndex = headers.indexOf('Action');
const videoIdColumnIndex = headers.indexOf('VideoID');
const rows = [...table.querySelectorAll('tbody tr')];
rows.forEach(row => {
const appendTo = row.children[0];
if (appendTo.querySelector(".export-segment")) return
if (videoIdColumnIndex != -1) {
videoId = row.children[videoIdColumnIndex].firstChild.textContent.trim();
}
if (!videoId) return;
const startTime = row.children[startColumnIndex].firstChild.textContent.trim();
const startTimeSeconds = stringToSec(startTime)
const endTime = row.children[endColumnIndex].textContent.trim();
const endTimeSeconds = stringToSec(endTime)
const actionType = row.children[actionTypeColumnIndex].firstChild.title.trim().toLowerCase();
const openLink = document.createElement('a');
openLink.textContent = "📂";
openLink.title = "Export segment to loadable URL"
openLink.href = createLink(videoId, startTimeSeconds, endTimeSeconds, actionType);
openLink.style.color = 'inherit';
openLink.classList.add("export-segment");
appendTo.prepend(openLink);
});
}
function createLink(videoId, startTime, endTime, actionType) {
const segmentObj = { actionType, category: "chooseACategory", segment: [startTime,endTime]}
return `https://youtube.com/watch?v=${videoId}#segments=[${JSON.stringify(segmentObj)}]`
}
(function (){
create()
document.addEventListener("newSegments", (event) => create());
})();
// ==UserScript==
// @name sb.ltn.fi imprecise times
// @namespace mchang.name
// @version 1.1.5
// @description Make all times on sb.ltn.fi imprecise
// @author michael mchang.name
// @match https://sb.ltn.fi/*
// @icon https://sb.ltn.fi/static/browser/logo.png
// @updateURL https://raw.githubusercontent.com/mchangrh/uscripts/main/sbltnfi/sbltnfi-imprecise-times.user.js
// @downloadURL https://raw.githubusercontent.com/mchangrh/uscripts/main/sbltnfi/sbltnfi-imprecise-times.user.js
// @grant none
// ==/UserScript==
// replace consequtive zeros at the end (that also follows a .) with nothing
const reduceTime = time => time.replace(/(?<=\.\d+)0+$/g, '');
function findTimes() {
const headers = [...document.querySelectorAll("thead th")].map((item) =>
item.textContent.trim()
);
// get all header indexes
const startIndex = headers.indexOf("Start");
const endIndex = headers.indexOf("End");
const lengthIndex = headers.indexOf("Length")
const table = document.querySelector("table.table");
table.querySelectorAll("tbody tr").forEach((row) => {
for (const index of [startIndex, endIndex, lengthIndex]) {
// skip if index is -1
if (index === -1) continue
let cell = row.children[index]
// loop into the deepest element to not break any other scripts
while (cell.firstChild) cell = cell.firstChild
// replace value
const oldValue = cell.textContent
cell.textContent = reduceTime(oldValue);
}
});
}
(function () {
"use strict";
findTimes();
document.addEventListener("newSegments", (event) => findTimes());
})();
// ==UserScript==
// @name Video Titles for sb.ltn.fi (with InnerTube)
// @namespace mchang.name
// @version 2.0.6
// @description Replaces the video ID with the video title in the 'Video ID' column.
// @author TheJzoli, michael mchang.name
// @match https://sb.ltn.fi/*
// @connect www.youtube.com
// @updateURL https://raw.githubusercontent.com/mchangrh/uscripts/main/sbltnfi/fork/sbltnfi-it-videotitle.user.js
// @downloadURL https://raw.githubusercontent.com/mchangrh/uscripts/main/sbltnfi/fork/sbltnfi-it-videotitle.user.js
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// ==/UserScript==
const videoIdAndRowElementObj = {};
(function() {
'use strict';
const animationCss = `
.loading {
display: inline-block;
vertical-align: middle;
width: 1em;
height: 1em;
margin-left: 0.5em;
}
.loading::after {
content: ' ';
display: block;
width: 0.9em;
height: 0.9em;
border-radius: 50%;
border: 0.1em solid #fff;
border-color: #cccc #cccc #cccc transparent;
animation: loader 1.2s linear infinite;
}
@keyframes loader {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}`;
GM_addStyle(animationCss);
[...document.querySelectorAll('table.table')].forEach(table => {
const headers = [...table.querySelectorAll('thead th')].map(item => item.textContent.trim());
if (headers.includes('VideoID') || headers.includes('Videoid')) {
const columnIndex = headers.includes('VideoID') ? headers.indexOf('VideoID') : headers.indexOf('Videoid');
if (headers.includes('VideoID')) {
[...table.querySelectorAll('thead th')][columnIndex].firstChild.innerText = "Video";
} else {
[...table.querySelectorAll('thead th')][columnIndex].innerText = "Video";
}
const rows = [...table.querySelectorAll('tbody tr')];
rows.forEach(row => {
const videoIdEl = row.children[columnIndex].firstChild;
const loadingEl = document.createElement('span');
loadingEl.classList.add("loading");
videoIdEl.appendChild(loadingEl);
const videoID = videoIdEl.innerText.trim();
if (videoID in videoIdAndRowElementObj) {
videoIdAndRowElementObj[videoID].push(videoIdEl);
} else {
videoIdAndRowElementObj[videoID] = [videoIdEl];
}
});
for (const [key, value] of Object.entries(videoIdAndRowElementObj)) {
callApi(key, value);
}
}
});
})();
function callApi(videoID, videoIdElArray) {
try {
const itRequest = JSON.stringify({
context: {
client: {
clientName: "WEB",
clientVersion: "2.20221215.04.01"
}
},
videoId: videoID,
})
function removeLoading() {
videoIdElArray.forEach(videoIdEl => {
videoIdEl.firstElementChild?.classList.remove('loading');
});
}
GM_xmlhttpRequest({
method: 'POST',
url: "https://www.youtube.com/youtubei/v1/player",
responseType: 'json',
data: itRequest,
timeout: 10000,
onload: (responseObject) => {
// Inject the new name in place of the old video ID
const title = responseObject?.response?.videoDetails?.title
if (title) {
videoIdElArray.forEach(videoIdEl => {
videoIdEl.innerText = title;
});
}
removeLoading()
},
onerror: removeLoading(),
ontimeout: removeLoading()
});
} catch (error) {
console.error(error);
}
}
// ==UserScript==
// @name Video Titles for sb.ltn.fi (with OEmbed)
// @namespace mchang.name
// @version 3.0.1
// @description Replaces the video ID with the video title in the 'Video ID' column.
// @author TheJzoli, michael mchang.name
// @match https://sb.ltn.fi/*
// @connect www.youtube.com
// @updateURL https://raw.githubusercontent.com/mchangrh/uscripts/main/sbltnfi/fork/sbltnfi-oembed-videotitle.user.js
// @downloadURL https://raw.githubusercontent.com/mchangrh/uscripts/main/sbltnfi/fork/sbltnfi-oembed-videotitle.user.js
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// ==/UserScript==
const videoIdAndRowElementObj = {};
(function() {
'use strict';
const animationCss = `
.loading {
display: inline-block;
vertical-align: middle;
width: 1em;
height: 1em;
margin-left: 0.5em;
}
.loading::after {
content: ' ';
display: block;
width: 0.9em;
height: 0.9em;
border-radius: 50%;
border: 0.1em solid #fff;
border-color: #cccc #cccc #cccc transparent;
animation: loader 1.2s linear infinite;
}
@keyframes loader {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}`;
GM_addStyle(animationCss);
[...document.querySelectorAll('table.table')].forEach(table => {
const headers = [...table.querySelectorAll('thead th')].map(item => item.textContent.trim());
if (headers.includes('VideoID') || headers.includes('Videoid')) {
const columnIndex = headers.includes('VideoID') ? headers.indexOf('VideoID') : headers.indexOf('Videoid');
if (headers.includes('VideoID')) {
[...table.querySelectorAll('thead th')][columnIndex].firstChild.innerText = "Video";
} else {
[...table.querySelectorAll('thead th')][columnIndex].innerText = "Video";
}
const rows = [...table.querySelectorAll('tbody tr')];
rows.forEach(row => {
const videoIdEl = row.children[columnIndex].firstChild;
const loadingEl = document.createElement('span');
loadingEl.classList.add("loading");
videoIdEl.appendChild(loadingEl);
const videoID = videoIdEl.innerText.trim();
if (videoID in videoIdAndRowElementObj) {
videoIdAndRowElementObj[videoID].push(videoIdEl);
} else {
videoIdAndRowElementObj[videoID] = [videoIdEl];
}
});
for (const [key, value] of Object.entries(videoIdAndRowElementObj)) {
callApi(key, value);
}
}
});
})();
function callApi(videoID, videoIdElArray) {
try {
function removeLoading() {
videoIdElArray.forEach(videoIdEl => {
videoIdEl.firstElementChild?.classList.remove('loading');
});
}
GM_xmlhttpRequest({
url: `https://www.youtube.com/oembed?url=youtube.com/watch?v=${videoID}&format=json`,
responseType: 'json',
timeout: 10000,
onload: (responseObject) => {
// Inject the new name in place of the old video ID
const title = responseObject?.title
if (title) {
videoIdElArray.forEach(videoIdEl => {
videoIdEl.innerText = title;
});
}
removeLoading()
},
onerror: removeLoading(),
ontimeout: removeLoading()
});
} catch (error) {
console.error(error);
}
}
// ==UserScript==
// @name sb.ltn.fi preset redirect + replace
// @namespace mchang.name
// @version 2.0.2
// @description make sure all sbltnfi links are filtered appropiately - redirect or replace hrefs
// @author michael mchang.name
// @match https://sb.ltn.fi/video/*
// @match https://sb.ltn.fi/uuid/*
// @match https://sb.ltn.fi/username/*
// @match https://sb.ltn.fi/userid/*
// @updateURL https://raw.githubusercontent.com/mchangrh/uscripts/main/sbltnfi/sbltnfi-preset-replace.user.js
// @downloadURL https://raw.githubusercontent.com/mchangrh/uscripts/main/sbltnfi/sbltnfi-preset-replace.user.js
// @icon https://sb.ltn.fi/static/browser/logo.png
// @run-at document-start
// @grant none
// ==/UserScript==
// custom filters for SBB
const videoFilter = {
"votes_min": 0,
"views_min": 1,
"sort": "starttime"
}
const userFilter = {
"votes_min": 0,
"views_min": 1
}
function substitute(url) {
// check which endpoint
const user = url.includes("/userid/") || url.includes("/username/")
const video = url.includes("/video/") || url.includes("/uuid/")
const filter = user ? userFilter
: video ? videoFilter
: null
if (!filter) return false
const newURL = new URL(url)
const params = Object.entries(filter)
if (params.length) return false // don't infinite loop if no params specified
for ([key, value] of params)
if (!newURL.searchParams.has(key))
newURL.searchParams.set(key, value)
const dest = newURL.toString()
return dest.length != url.length ? dest : false // only redirect if difference in URL
}
function redirect() {
const url = window.location.toString()
// check if there are search params already
const params = new URL(url).searchParams.toString()
if (params.length) return
const newURL = substitute(url)
if (!newURL) return
window.location.replace(newURL)
}
const replaceLinks = () =>
document.querySelectorAll("a").forEach(link => {
const newhref = substitute(link.href)
if (newhref) link.setAttribute("href", newhref)
})
redirect()
// wait 200ms for document-end
window.addEventListener("DOMContentLoaded", () => setTimeout(replaceLinks, 200))
// ==UserScript==
// @name sb.ltn.fi refresh segment
// @namespace mchang.name
// @version 1.2.2
// @description Refresh a single segment
// @author michael mchang.name
// @match https://sb.ltn.fi/*
// @icon https://sb.ltn.fi/static/browser/logo.png
// @grant GM_xmlhttpRequest
// @updateURL https://raw.githubusercontent.com/mchangrh/uscripts/main/sbltnfi/sbltnfi-refresh.user.js
// @downloadURL https://raw.githubusercontent.com/mchangrh/uscripts/main/sbltnfi/sbltnfi-refresh.user.js
// ==/UserScript==
function refreshRow(event) {
const uuid = event.target.dataset.uuid;
event.target.innerText = "⏲️";
GM_xmlhttpRequest({
method: "GET",
url: `https://sponsor.ajay.app/api/segmentInfo?UUID=${uuid}`,
responseType: "json",
timeout: 10000,
onload: (res) => updateRow(res.response[0], uuid),
onerror: (res) => updateRow(false, uuid),
ontimeout: (res) => updateRow(false, uuid)
});
}
function createButtons() {
const table = document.querySelector("table.table");
const headers = [...table.querySelectorAll("thead th")].map((item) =>
item.textContent.trim()
);
const uuidColumnIndex = headers.indexOf("UUID");
if (uuidColumnIndex === -1) return;
table.querySelectorAll("tbody tr").forEach((row) => {
const cellEl = row.children[uuidColumnIndex];
// check if refresh button exists
if (cellEl.querySelector("#mchang_refresh")) return;
const UUID = cellEl.querySelector("textarea").value;
const button = document.createElement("button");
button.id = "mchang_refresh";
button.innerText = "🔄";
button.dataset.uuid = UUID;
button.addEventListener("click", refreshRow);
cellEl.appendChild(button);
});
}
const oldChildrenFind = (children, textMatch) =>
Array.from(children).find((elem) => elem.innerText == textMatch);
function updateRow(data, uuid) {
const table = document.querySelector("table");
const headers = [...table.querySelectorAll("thead th")].map((item) =>
item.textContent.trim()
);
const uuidColumnIndex = headers.indexOf("UUID");
if (uuidColumnIndex === -1) return;
table.querySelectorAll("tbody tr").forEach((row) => {
const rowChildren = row.children;
const cellEl = rowChildren[uuidColumnIndex];
if (cellEl.querySelector("textarea").value === uuid) {
if (!data) return cellEl.querySelector("#mchang_refresh").innerText = "⚠️";
cellEl.querySelector("#mchang_refresh").innerText = "✅";
// update data
// votes
const votesColumnIndex = headers.indexOf("Votes");
const oldChildren = rowChildren[votesColumnIndex].children;
let newVotes = data.votes;
if (data.votes === -2 && !oldChildrenFind(oldChildren, "❌")) {
newVotes += "❌";
}
if (data.locked === 1 && !oldChildrenFind(oldChildren, "🔒")) {
newVotes += "🔒";
}
rowChildren[votesColumnIndex].childNodes[0].nodeValue = newVotes;
// views
rowChildren[headers.indexOf("Views")].textContent = data.views;
// workaround for colour categories
const categoryColumn = rowChildren[headers.indexOf("Category")];
const categorySpan = categoryColumn.querySelector(".mruy_sbcc");
if (categorySpan) {
categorySpan.textContent = data.category;
} else {
categoryColumn.innerText = data.category;
}
}
});
}
(function () {
"use strict";
createButtons();
document.addEventListener("newSegments", (event) => createButtons());
})();
// ==UserScript==
// @name sb.ltn.fi UUID requiredSegment
// @namespace mchang.name
// @version 2.2.5
// @description Generate a link to requiredSegment from UUID
// @author michael mchang.name
// @updateURL https://raw.githubusercontent.com/mchangrh/uscripts/main/sbltnfi/sbltnfi-requiredSegments.user.js
// @downloadURL https://raw.githubusercontent.com/mchangrh/uscripts/main/sbltnfi/sbltnfi-requiredSegments.user.js
// @match https://sb.ltn.fi/*
// @icon https://sb.ltn.fi/static/browser/logo.png
// ==/UserScript==
function createButtons() {
document.querySelectorAll("table.table").forEach((table) => {
const headers = [...table.querySelectorAll("thead th")].map((item) =>
item.textContent.trim()
);
const uuidColumnIndex = headers.indexOf("UUID");
if (uuidColumnIndex === -1) return;
table.querySelectorAll("tbody tr").forEach((row) => {
const cellEl = row.children[uuidColumnIndex];
if (cellEl.querySelector("#mchang_requiredsegments")) return;
const UUID = cellEl.querySelector("textarea").value;
const button = document.createElement("button");
button.id = "mchang_requiredsegments";
button.innerText = "sb/";
button.addEventListener("click", () =>
navigator.clipboard.writeText(`https://sb.mchang.xyz/${UUID}`)
);
cellEl.appendChild(button);
});
});
}
(function () {
"use strict";
createButtons();
document.addEventListener("newSegments", (event) => createButtons());
})();
const stringToSec = (str, addMs = true) => {
let [s, ms] = str.split('.')
ms = ms ?? 0
// https://stackoverflow.com/a/45292588
t = s.split(':').reduce((acc,time) => (60 * acc) + +time)
return addMs ? t + '.' + ms : t;
}
// ==UserScript==
// @name Add frames to YouTube time
// @namespace mchang.name
// @version 1.0.1
// @description Add frames to YouTube time
// @author michael mchang.name
// @match https://www.youtube.com/*
// @icon https://www.google.com/s2/favicons?domain=youtube.com
// @grant none
// @updateURL https://raw.githubusercontent.com/mchangrh/uscripts/main/yt/yt-frames.user.js
// @downloadURL https://raw.githubusercontent.com/mchangrh/uscripts/main/yt/yt-frames.user.js
// @require https://neuter.mchang.xyz/require/wfke.js
// ==/UserScript==
function setFrames() {
if (document.getElementById("mchang-ytfps") === null) {
const video = document.querySelector('#movie_player');
const resolution = video.getStatsForNerds().resolution;
const fps = resolution
.split("/")[0]
.split("@")[1].trim();
const spanEl = document.createElement('span');
spanEl.id = "mchang-ytfps";
spanEl.class = "ytp-time-duration";
spanEl.textContent = `@${fps}fps`;
const oldTime = document.querySelector('.ytp-time-current').parentElement;
oldTime.append(spanEl);
}
}
const awaitPlayer = () => wfke(".ytp-time-display", setFrames)
document.body.addEventListener("yt-navigate-finish", (event) => awaitPlayer() );
// ==UserScript==
// @name Add seek precision to YouTube
// @namespace mchang.name
// @version 1.0.1
// @description Add additional seeking options on YouTube
// @author michael mchang.name
// @match https://www.youtube.com/*
// @icon https://www.google.com/s2/favicons?domain=youtube.com
// @grant none
// @updateURL https://raw.githubusercontent.com/mchangrh/uscripts/main/yt/yt-moreseek.user.js
// @downloadURL https://raw.githubusercontent.com/mchangrh/uscripts/main/yt/yt-moreseek.user.js
// ==/UserScript==
const seekTo = (amt) => player.seekBy(amt);
const suppress = (e) => e.preventDefault();
function listenKey(e) {
// ctrl = 10
// default = 1
// shift = 0.1
const key = e.key;
const step = (e.shiftKey) ? 0.1
: (e.ctrlKey) ? 10
: 1;
if (key == "a" || key == "A") {
suppress(e);
seekTo(-1 * step);
} else if (key == "d" || key == "D") {
suppress(e);
seekTo(step);
}
}
const video = document.querySelector('video');
const player = document.querySelector('#movie_player');
document.addEventListener("keydown", listenKey);
// ==UserScript==
// @name Add milliseconds to YouTube time
// @namespace mchang.name
// @version 1.1.1
// @description add exact milliseconds to YouTube time
// @author michael mchang.name
// @match https://www.youtube.com/*
// @icon https://www.google.com/s2/favicons?domain=youtube.com
// @grant none
// @updateURL https://raw.githubusercontent.com/mchangrh/uscripts/main/yt/yt-mstime.user.js
// @downloadURL https://raw.githubusercontent.com/mchangrh/uscripts/main/yt/yt-mstime.user.js
// @require https://neuter.mchang.xyz/require/wfke.js
// ==/UserScript==
function setMs() {
if (document.getElementById("mchang-ytms") === null) {
const video = document.querySelector('video');
const ms = (video.duration+"").split(".").pop() ?? 0
const oldTime = document.querySelector('.ytp-time-duration');
const spanEl = document.createElement('span');
spanEl.id = "mchang-ytms";
spanEl.class = "ytp-time-duration"
spanEl.textContent = "."+ms
oldTime.after(spanEl);
}
}
const awaitPlayer = () => wfke(".ytp-time-display", setMs)
document.body.addEventListener("yt-navigate-finish", (event) => awaitPlayer() );
// ==UserScript==
// @name Warn on Post-Live Manifestless
// @namespace mchang.name
// @version 1.1.1
// @description adds a big red warning to the top of the screen when video is post-live manifestless
// @author michael mchang.name
// @match https://www.youtube.com/*
// @icon https://www.google.com/s2/favicons?domain=youtube.com
// @updateURL https://raw.githubusercontent.com/mchangrh/uscripts/main/yt/yt-warn-postlive.user.js
// @downloadURL https://raw.githubusercontent.com/mchangrh/uscripts/main/yt/yt-warn-postlive.user.js
// @require https://neuter.mchang.xyz/require/wfke.js
// @grant none
// ==/UserScript==
let playerDetail;
function warn() {
if (document.getElementById("postlive-warning") !== null) return;
const cont = document.querySelector('ytd-masthead#masthead');
const spanEl = document.createElement('span');
spanEl.id = "postlive-warning";
spanEl.textContent = "!!!!! post-live manifestless !!!!!";
spanEl.style = `
font-size: 16px;
text-align: center;
padding-top: 5px;
display: block;
color: #fff;
background: #f00;
width: 100%;
height: 30px;`
cont.prepend(spanEl);
}
const checkRequired = () => {
if (playerDetail && playerDetail.getVideoData().isManifestless) warn()
}
const awaitMasthead = () => wfke("ytd-masthead#masthead", checkRequired)
const hookDetail = (e) => {
playerDetail = e.detail
awaitMasthead()
}
document.addEventListener("yt-navigate-finish", (event) => awaitMasthead() );
document.addEventListener("yt-player-updated", hookDetail)
// ==UserScript==
// @name Warn on Required Segments
// @namespace mchang.name
// @version 1.1.1
// @description adds a big red warning to the top of the screen when requiredSegment is present
// @author michael mchang.name
// @match https://www.youtube.com/*
// @icon https://www.google.com/s2/favicons?domain=youtube.com
// @updateURL https://raw.githubusercontent.com/mchangrh/uscripts/main/yt/yt-warn-reqseg.user.js
// @downloadURL https://raw.githubusercontent.com/mchangrh/uscripts/main/yt/yt-warn-reqseg.user.js
// @require https://neuter.mchang.xyz/require/wfke.js
// @grant none
// ==/UserScript==
function setupButton(segmentID) {
if (document.getElementById("reqseg-warning")) return
const cont = document.querySelector('ytd-masthead#masthead');
const spanEl = document.createElement('span');
spanEl.id = "reqseg-warning";
spanEl.textContent = "!!!!! Required Segment: " + segmentID + " !!!!!";
spanEl.style = `
font-size: 16px;
text-align: center;
padding-top: 5px;
display: block;
color: #fff;
background: #f00;
width: 100%;
height: 30px;`
cont.prepend(spanEl);
}
function checkRequired() {
const hash = new URL(document.URL)?.hash
if (!hash) return
const hasReqSegm = hash.startsWith("#requiredSegment");
if (hasReqSegm) {
const segmentID = hash.match(/=([\da-f]+)/)?.[1]
setupButton(segmentID)
}
}
const awaitMasthead = () => wfke("ytd-masthead#masthead", checkRequired)
document.body.addEventListener("yt-navigate-finish", (event) => checkRequired());
@chirag127
Copy link

sbltnfi-clickable-starttime-fork.user.js is not working after force refresh.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment