Skip to content

Instantly share code, notes, and snippets.

@Krashgamer
Last active June 24, 2023 19:24
Show Gist options
  • Save Krashgamer/c8ed1bc131a214e1fcc84338b73f11b7 to your computer and use it in GitHub Desktop.
Save Krashgamer/c8ed1bc131a214e1fcc84338b73f11b7 to your computer and use it in GitHub Desktop.

Fork of Userscripts for sb.ltn.fi (forceupdate) by TheJzoli

  • sb.ltn.fi.forceupdate.user.js
    Fetches the segments and updated info for the video when Loading the Page, so that you don't have to wait for the video page to update. Install
// ==UserScript==
// @name Force-update on Page Load
// @namespace sb.ltn.fi.force.update
// @version 1.0.1
// @description Fetches the segments and updated info for the video, so that you don't have to wait for the video page to update.
// @author TheJzoli
// @match https://sb.ltn.fi/video/*
// @match https://sb.ltn.fi/uuid/*
// @icon https://sb.ltn.fi/static/browser/logo.png
// @grant GM_xmlhttpRequest
// @grant GM_setClipboard
// @connect sponsor.ajay.app
// @updateURL https://gist.github.com/Krashgamer/c8ed1bc131a214e1fcc84338b73f11b7/raw/sb.ltn.fi.forceupdate.user.js
// @downloadURL https://gist.github.com/Krashgamer/c8ed1bc131a214e1fcc84338b73f11b7/raw/sb.ltn.fi.forceupdate.user.js
// ==/UserScript==
(function() {
'use strict';
const SBLTNFI_TO_API_PARAMS = {views_min: 'minViews', views_max: 'maxViews', votes_min: 'minVotes', votes_max: 'maxVotes', category: 'category'};
const BASEAPIURL = 'https://sponsor.ajay.app/api';
const ignoreParams = ['shadowhidden', 'uuid', 'username', 'user', 'sort', 'page'];
const exclude = ['input', 'textarea'];
const listNextToVideoEmbed = document.getElementsByClassName('list-group')[document.getElementsByClassName('list-group').length - 1];
// Force-update when Page Finished loading
addRefreshButton();
window.addEventListener('load', function () {
const videoID = checkPageVideoID();
if (!videoID) return;
getAndUpdateSegments(videoID);
getAndUpdateLocks(videoID);
}
);
/**
* Gets all the video's segments, then calls the {@link updateTable} function, and updates the submission amount.
* @param {string} videoID ID of the video whose segments we want to fetch
*/
function getAndUpdateSegments(videoID) {
document.body.style.cursor = 'progress';
let segmentsArray = [];
const searchParams = new URLSearchParams(location.search);
const filters = buildFilterParam(searchParams);
let page = 0;
let segmentCount = 0;
const callback = (responseObject) => {
if (responseObject.status !== 200) {
if (responseObject.status === 404) {
window.alert('No segments found for video');
} else {
window.alert(`API returned a status of ${responseObject.status} when getting segments`);
}
document.body.style.cursor = 'default';
return;
}
segmentCount = responseObject.response.segmentCount;
segmentsArray = segmentsArray.concat(responseObject.response.segments);
if (segmentCount > 10 && (page+1)*10 < segmentCount) {
getSegments(videoID, filters, ++page, callback);
} else if (segmentsArray.length === segmentCount) {
// Update table once all segments have been fetched
updateTable(segmentsArray, searchParams);
// Update submission amount
getDivThatStartsWithText('Query results:').textContent = `Query results: ${responseObject.response.segmentCount}`;
// If total submissions is over current query's segment count, don't update it.
if (listNextToVideoEmbed.firstElementChild.textContent.split(':')[1].trim() < responseObject.response.segmentCount) {
listNextToVideoEmbed.firstElementChild.textContent = `Submissions: ${responseObject.response.segmentCount}`;
}
// Update ignored amount
let hiddenAmount = 0;
let ignoredAmount = 0;
segmentsArray.forEach(segment => {
hiddenAmount += segment.hidden;
ignoredAmount += segment.shadowHidden || segment.votes <= -2 ? 1 : 0;
});
listNextToVideoEmbed.children[1].textContent = `Ignored: ${ignoredAmount} `;
listNextToVideoEmbed.children[1].appendChild(createSpan('Segments that are also not sent to users, but don\'t count as ignored', `+ ${hiddenAmount} ❓`));
// Remove the "no submissions exist" alert if it exists. This would be on the page if the page didn't have any segments before updating.
document.querySelector("[role=alert]")?.remove();
document.body.style.cursor = 'default';
// Fire event to signal other scripts of force refresh
const event = new CustomEvent("forceRefresh");
document.dispatchEvent(event);
}
};
getSegments(videoID, filters, page, callback); // TODO only call for specific pages based on SBB page
}
/**
* Calls the SponsorBlock API
* @param {string} videoID ID of the video whose segments we want to fetch
* @param {string} filters Filter params e.g. minVotes etc.
* @param {number} page The page number we want from the API. 1 page is 10 segments
* @param {function} callback callback function to call when onload triggers
*/
function getSegments(videoID, filters, page, callback) {
GM_xmlhttpRequest({
method: 'GET',
url: `${BASEAPIURL}/searchSegments?videoID=${videoID+filters}&page=${page}`,
responseType: 'json',
timeout: 10000,
onload: callback,
onerror: () => {
document.body.style.cursor = 'default';
window.alert('An error occurred getting the segments!');
},
ontimeout: () => {
document.body.style.cursor = 'default';
window.alert('Segments request timed out! API didn\'t answer in time.');
}
});
}
/**
* Gets the video's locked categories and updates them.
* @param {string} videoID ID of the video whose locked categories we want
*/
function getAndUpdateLocks(videoID) {
const skipCallback = (responseObject) => {
updateLockedCategories(responseObject, 'Locked skips: ', 2);
};
const muteCallback = (responseObject) => {
updateLockedCategories(responseObject, 'Locked mutes: ', 3);
};
const fullCallback = (responseObject) => {
updateLockedCategories(responseObject, 'Locked full: ', 4);
};
getLocks(videoID, 'skip', skipCallback);
getLocks(videoID, 'mute', muteCallback);
getLocks(videoID, 'full', fullCallback);
}
/**
* Calls the SponsorBlock API to get the locked categories with the specified actionType for the specified video.
* @param {string} videoID ID of the video whose locked categories we want
* @param {string} actionType Action type of the locked categories
* @param {function} callback callback function to call when the request loads
*/
function getLocks(videoID, actionType, callback) {
GM_xmlhttpRequest({
method: 'GET',
url: `${BASEAPIURL}/lockCategories?videoID=${videoID}&actionTypes=["${actionType}"]`,
responseType: 'json',
timeout: 10000,
onload: callback,
onerror: () => {
window.alert(`An error occurred getting the locked categories with action type '${actionType}'!`);
},
ontimeout: () => {
window.alert(`Locked categories (with action type '${actionType}') request timed out! API didn't answer in time.`);
}
});
}
/**
* Updates one of the actionTypes locked categories next to the embed player.
* @param {Object} responseObject onload response from the GM_xmlhttpRequest
* @param {string} startingString starting part of the locked categories string
* @param {number} index index of the actionType in the list next to the embed player
*/
function updateLockedCategories(responseObject, startingString, index) {
if (responseObject.status !== 200 && responseObject.status !== 404) {
let actionType = startingString.slice(7, -3);
if (actionType.length === 3) actionType += 'l';
window.alert(`API returned a status of ${responseObject.status} when getting locked categories with action type ${actionType}`);
}
let lockedString = startingString;
const lockedStringStartingLength = lockedString.length;
responseObject.response?.categories?.forEach((category, i) => {
if (i !== 0) {
lockedString += ', ';
}
lockedString += category;
});
lockedString += lockedString.length === lockedStringStartingLength ? '—' : '';
listNextToVideoEmbed.children[index].textContent = lockedString;
}
/**
* Updates the table with the new data from the provided array
* @param {Object[]} segmentsArray Array of the segments fetched from the API
* @param {URLSearchParams} searchParams Search parameters of the current page
*/
function updateTable(segmentsArray, searchParams) {
[...document.querySelectorAll('table')].forEach(table => {
const headers = [...table.querySelectorAll('thead th')].map(item => item.textContent.trim());
if (headers.indexOf('UUID') === -1) {
return;
}
const rows = [...table.querySelectorAll('tbody tr')];
rows.forEach(row => updateRow(row, headers, segmentsArray)); // Update info for the old segments on the page
// Don't add new segments if on a page other than 1 (TODO remove when page support gets added)
let currentPage = parseInt(searchParams.get('page'));
currentPage = isNaN(currentPage) ? 1 : currentPage;
if (currentPage !== 1) {
return;
}
// Add new segments to the page
const lastUpdateElement = getDivThatStartsWithText('Last update:');
const lastUpdateTime = dateToMillis(lastUpdateElement.textContent.trim().slice(13, 32).replaceAll(' ', 'T') + 'Z');
const newSegments = segmentsArray.filter((segment) => {
if (segment.timeSubmitted > lastUpdateTime) {
let addSegment = true;
if (searchParams.has('shadowhidden')) {
addSegment = addSegment && (segment.shadowHidden.toString() === searchParams.get('shadowhidden'));
}
if (searchParams.has('uuid')) {
addSegment = addSegment && (segment.UUID === searchParams.get('uuid'));
}
if (searchParams.has('user')) {
addSegment = addSegment && (segment.userID === searchParams.get('user'));
}
return addSegment;
}
return false;
});
const tableBody = table.getElementsByTagName('tbody')[0];
newSegments.forEach(segment => {
for (const row of rows) { // Check if the new segment is already on the page from a previous force refresh
if (row.children[headers.indexOf('UUID')].firstElementChild.value === segment.UUID) {
return;
}
}
tableBody.insertAdjacentElement('afterbegin', createRowElement(segment));
});
fixRows(tableBody);
// Fire event to signal other scripts of new segments
if (newSegments.length !== 0) {
const event = new CustomEvent("newSegments");
document.dispatchEvent(event);
}
if (newSegments.length > 0 && searchParams.has('username')) { // Notify user that filtering by username isn't supported
window.alert('Filtering by username is not supported due to API limitations. \nNew added segments may be from different users');
}
/* TODO Add support for different sort options and pages
switch (searchParams.get('sort')) {
case 'timesubmitted': // old -> new
break;
case 'starttime':
break;
case 'endtime':
break;
case 'length':
break;
case 'votes':
break;
case 'views':
break;
case 'category':
break;
case 'shadowhidden':
break;
case 'uuid':
break;
case 'username':
break;
case 'actiontype':
break;
case 'hidden':
break;
case 'userid':
break;
case '-userid':
break;
case '-starttime':
break;
case '-endtime':
break;
case '-length':
break;
case '-votes':
break;
case '-views':
break;
case '-category':
break;
case '-shadowhidden':
break;
case '-uuid':
break;
case '-username':
break;
case '-actiontype':
break;
case '-hidden':
break;
case '-timesubmitted': // new -> old
default: // same as -timesubmitted
}
*/
});
}
/**
* Updates the data for the segment in this row
* @param {HTMLTableRowElement} row Row to be updated
* @param {string[]} headers String array of the table's headers
* @param {Object[]} segmentsArray Array of the segments fetched from the API
*/
function updateRow(row, headers, segmentsArray) {
const uuid = row.children[headers.indexOf('UUID')].firstElementChild.value;
const oldSegmentIndex = segmentsArray.findIndex(segment => segment.UUID === uuid); // Find the index for the segment on the page, so we can get its updated info from the API
if (oldSegmentIndex === -1) {
return; // If there is a segment UUID on the page that isn't on the API.
}
// Update Votes column
const votesEl = row.children[headers.indexOf('Votes')];
let underNegTwoVotesSpan;
let lockedSpan;
[...votesEl.children].forEach(el => { // Check for existing icons and save their element
if (el.textContent === '❌') {
underNegTwoVotesSpan = el;
return;
}
if (el.textContent === '🔒') {
lockedSpan = el;
}
});
votesEl.firstChild.textContent = segmentsArray[oldSegmentIndex].votes; // Update votes from API
if (segmentsArray[oldSegmentIndex].votes <= -2) { // Add X symbol if votes under -1 and it already isn't there
if (!underNegTwoVotesSpan) {
votesEl.firstChild.after(createSpan('This segment is not sent to users', '❌'));
}
} else if (underNegTwoVotesSpan) { // Delete X symbol if it is there and votes are over -2
underNegTwoVotesSpan.remove();
}
if (segmentsArray[oldSegmentIndex].locked) { // Add locked symbol if segment is locked and it already isn't there
if (!lockedSpan) {
votesEl.firstChild.after(createSpan('This segment is locked by a VIP', '🔒'));
}
} else if (lockedSpan) { // Delete locked symbol if it is there and the segment isn't locked
lockedSpan.remove();
}
// Update Views column
const viewsEl = row.children[headers.indexOf('Views')];
viewsEl.textContent = segmentsArray[oldSegmentIndex].views;
// Update Category column
const newCategory = segmentsArray[oldSegmentIndex].category;
const categoryEl = row.children[headers.indexOf('Category')];
const span = categoryEl.getElementsByTagName('span');
const categorySpanEl = span[0]?.id === 'colorSquare' ? span[1]: span[0]; // colored categories userscript or chapter span
const buttons = categoryEl.getElementsByTagName('button');
const categoryButton = buttons.length !== 0 && buttons[0].hasAttribute('id') ? buttons[0] : undefined; // VIP tools userscript
if (newCategory !== 'chapter') { // Chapters cannot be changed to other categories
if (categoryButton) { // Check for VIP tools userscript button
categoryButton.firstChild.textContent = newCategory;
}
if (categorySpanEl && categorySpanEl.classList[0] === 'mruy_sbcc') { // Check for colored categories userscript
const newClass = categorySpanEl.classList[1].slice(0, 10) + segmentsArray[oldSegmentIndex].category;
categorySpanEl.classList.replace(categorySpanEl.classList[1], newClass);
categorySpanEl.textContent = newCategory;
} else if (!categoryButton) { // If no colored categories, or vip tools userscripts.
if (categoryEl.firstElementChild?.hasAttribute('id')) { // check for colored square span
categoryEl.childNodes[1].textContent = newCategory;
} else { // if no other script that messes with the category column
categoryEl.firstChild.textContent = newCategory;
}
}
}
// Update Shadowhidden column
const shadowHiddenEl = row.children[headers.indexOf('Shadowhidden')];
shadowHiddenEl.innerHTML = ''; // empty the element
shadowHiddenEl.appendChild(segmentsArray[oldSegmentIndex].shadowHidden ? createSpan('This segment has been shadowhidden.', '❌') : document.createTextNode('—'));
// Update Hidden column
const hiddenEl = row.children[headers.indexOf('Hidden')];
hiddenEl.innerHTML = ''; // empty the element
hiddenEl.appendChild(segmentsArray[oldSegmentIndex].hidden ? createSpan('This segment is hidden due to video duration change.', '❌') : document.createTextNode('—'));
// Update S/H column if Hide Columns userscript is being used
const shIndex = headers.indexOf('S/H');
if (shIndex !== -1) {
const shEl = row.children[shIndex];
shEl.textContent = segmentsArray[oldSegmentIndex].shadowHidden || segmentsArray[oldSegmentIndex].hidden ? '❌' : '—';
}
}
/**
* Builds a string from the given parameters.
* Transforms the parameters from the way they appear in SBB's url, to what SponsorBlock's API accepts.
* @param {URLSearchParams} searchParams The parameters from SBB's url with which we want to call the API
* @returns {string} Parameters string that should be appended to the API request url
*/
function buildFilterParam(searchParams) {
let paramString = '';
searchParams.forEach((value, key) => {
if (ignoreParams.includes(key)) {
return;
}
paramString += `&${SBLTNFI_TO_API_PARAMS[key]}=${value}`;
});
return paramString;
}
/**
* Returns the div element that starts with the specified text.
* @param {string} text Text we want to search
* @returns {HTMLDivElement}
*/
function getDivThatStartsWithText(text) {
const divElements = Array.from(document.getElementsByTagName('div'));
const result = divElements.filter(element => element.innerHTML.trim().slice(0, text.length) === text);
if (result.length !== 0) {
return result[0];
}
}
/**
* Converts milliseconds to a date.
* @param {number | string} millis Milliseconds to be converted to a date
* @returns {string} The date in a YYYY-MM-DD HH:MM:SS format
*/
function millisToDate(millis) {
return new Date(millis).toISOString().slice(0, -5).replaceAll('T', ' ');
}
/**
* Converts date string to milliseconds.
* @param {string} date Date string. Date should conform to the ISO 8601 format "YYYY-MM-DDTHH:mm:ssZ"
* @returns {number}
*/
function dateToMillis(date) {
return new Date(date).getTime();
}
/**
* Converts the time from seconds to hours, minutes, and seconds.
* @param {number | string} timeInSeconds Seconds to be converted
* @returns {string} The time in H:MM:SS.mmmmmm format
*/
function secondsToTime(timeInSeconds) {
const pad = num => { return ('0' + num).slice(-2) },
time = parseFloat(timeInSeconds).toFixed(6),
hours = Math.floor(time / 60 / 60),
minutes = Math.floor(time / 60) % 60,
seconds = Math.floor(time - minutes * 60),
milliseconds = time.split('.')[1];
return hours + ':' + pad(minutes) + ':' + pad(seconds) + (milliseconds === '000000' ? '' : '.' + milliseconds);
}
/**
* Creates a row element and returns it
* @param {Object} segment Segment from the API whose data is used to build the row
* @returns {HTMLTableRowElement}
*/
function createRowElement(segment) {
const row = document.createElement('tr');
const submittedColumn = document.createElement('td');
submittedColumn.textContent = millisToDate(segment.timeSubmitted);
const startColumn = document.createElement('td');
startColumn.textContent = secondsToTime(segment.startTime);
const endColumn = document.createElement('td');
endColumn.textContent = secondsToTime(segment.endTime);
const lengthColumn = document.createElement('td');
lengthColumn.textContent = secondsToTime(segment.endTime - segment.startTime);
const votesColumn = document.createElement('td');
votesColumn.textContent = segment.votes;
if (segment.votes <= -2) {
votesColumn.appendChild(createSpan('This segment is not sent to users', '❌'));
}
if (segment.locked) {
votesColumn.appendChild(createSpan('This segment is locked by a VIP', '🔒'));
}
getAndAddVipStatus(votesColumn, segment.userID);
const viewsColumn = document.createElement('td');
viewsColumn.textContent = segment.views;
const categoryColumn = document.createElement('td');
let hasCategoryColorsScript = false;
[...document.getElementsByTagName('style')].forEach(styleEl => { // Checks if the user has the colored categories script and adds the appropriate classes to the element
[...styleEl.childNodes].forEach(node => {
if (node?.data?.includes('mruy_sbcc')) {
hasCategoryColorsScript = true;
const spanEl = document.createElement('span');
spanEl.className = `mruy_sbcc mruy_sbcc_${segment.category}`;
if (segment.category !== 'chapter') spanEl.textContent = segment.category;
categoryColumn.appendChild(spanEl);
}
});
});
if (segment.category === 'chapter' && hasCategoryColorsScript) { // chapter elements have an additional span in them to have the chapter's text appear in the title.
categoryColumn.firstElementChild.appendChild(createSpan(segment.description, segment.category)); // if category colors script is present another extra span is added and we need to account for that
} else if (segment.category === 'chapter') {
categoryColumn.appendChild(createSpan(segment.description, segment.category));
}
if (!hasCategoryColorsScript && categoryColumn.childElementCount === 0) categoryColumn.textContent = segment.category;
const shadowhiddenColumn = document.createElement('td');
shadowhiddenColumn.appendChild(segment.shadowHidden ? createSpan('This segment has been shadowhidden.', '❌') : document.createTextNode('—'));
const uuidColumn = createTextAreaColumnElement(segment.UUID, 'UUID');
const usernameColumn = document.createElement('td');
usernameColumn.textContent = '—'; // empty placeholder until username is fetched
getAndAddUsername(usernameColumn, segment.userID);
const actiontypeColumn = document.createElement('td');
switch (segment.actionType) {
case 'skip':
actiontypeColumn.appendChild(createSpan('Skip', '⏭'));
break;
case 'poi':
actiontypeColumn.appendChild(createSpan('Highlight', '✨️'));
break;
case 'full':
actiontypeColumn.appendChild(createSpan('Full video', '♾'));
break;
case 'mute':
actiontypeColumn.appendChild(createSpan('Mute', '🔇'));
break;
case 'chapter':
actiontypeColumn.appendChild(createSpan('Chapter', '🏷️'));
break;
default:
actiontypeColumn.appendChild(document.createTextNode('—'));
}
const hiddenColumn = document.createElement('td');
hiddenColumn.appendChild(segment.hidden ? createSpan('This segment is hidden due to video duration change.', '❌') : document.createTextNode('—'));
const userIdColumn = createTextAreaColumnElement(segment.userID, 'UserID');
row.append(submittedColumn, startColumn, endColumn, lengthColumn, votesColumn, viewsColumn, categoryColumn, shadowhiddenColumn, uuidColumn, usernameColumn, actiontypeColumn, hiddenColumn, userIdColumn);
if (document.getElementById('checkboxSpecial')?.checked) { // Check if the user has Hide Columns script's S/H column
const shColumn = document.createElement('td');
shColumn.textContent = segment.shadowHidden || segment.hidden ? '❌' : '—';
row.appendChild(shColumn);
}
return row;
}
/**
* Creates a span element with a title and text content
* @param {string} title Title for the span
* @param {string} content Text to be put in the span
* @returns {HTMLSpanElement}
*/
function createSpan(title, content) {
const newSpan = document.createElement('span');
newSpan.title = title;
newSpan.textContent = content;
return newSpan;
}
/**
* Creates a td element with a textarea, a button, and a link.
* @param {string} value Value of the textarea
* @param {string} name Name for the textarea. Is the same as the column's header.
* @returns {HTMLTableCellElement}
*/
function createTextAreaColumnElement(value, name) {
const tdEl = document.createElement('td');
const textAreaEl = document.createElement('textarea');
textAreaEl.textContent = value;
textAreaEl.className = 'form-control';
textAreaEl.name = name;
textAreaEl.readOnly = true;
const buttonEl = document.createElement('button');
buttonEl.textContent = '✂';
buttonEl.addEventListener('click', () => GM_setClipboard(value));
const linkEl = document.createElement('a');
linkEl.textContent = '🔗';
linkEl.href = `/${name.toLowerCase()}/${value}/`;
tdEl.append(textAreaEl, buttonEl, linkEl);
return tdEl;
}
/**
* Checks if the user is a VIP and appends the crown symbol to the given element.
* @param {HTMLTableCellElement} votesColumn The element we want to append the crown symbol to
* @param {string} userID The userID of the user we want to check
*/
function getAndAddVipStatus(votesColumn, userID) {
GM_xmlhttpRequest({
method: 'GET',
url: `${BASEAPIURL}/userinfo?publicUserID=${userID}&value=vip`,
responseType: 'json',
timeout: 10000,
onload: (res) => {
if (res.status !== 200) {
console.warn('Couldn\'t get VIP status. API returned status ' + res.status);
appendRetryButtonToColumn(votesColumn, userID, 'Failed to get VIP status. Press to retry', getAndAddVipStatus);
return;
}
if (res.response.vip) {
votesColumn.appendChild(createSpan('This user is a VIP', '👑'));
}
},
onerror: () => {
console.warn('Couldn\'t get VIP status due to an error');
appendRetryButtonToColumn(votesColumn, userID, 'Failed to get VIP status. Press to retry', getAndAddVipStatus);
},
ontimeout: () => {
console.warn('Couldn\'t get VIP status due to timeout');
appendRetryButtonToColumn(votesColumn, userID, 'Failed to get VIP status. Press to retry', getAndAddVipStatus);
}
});
}
/**
* Gets the username of the specified user and adds it to the given element.
* @param {HTMLTableCellElement} usernameColumn The element we want to add the username to
* @param {string} userID The userID of the user whose username we want
*/
function getAndAddUsername(usernameColumn, userID) {
GM_xmlhttpRequest({
method: 'GET',
url: `${BASEAPIURL}/userinfo?publicUserID=${userID}&value=userName`,
responseType: 'json',
timeout: 10000,
onload: (res) => {
if (res.status !== 200) {
console.warn('Couldn\'t get username. API returned status ' + res.status);
appendRetryButtonToColumn(usernameColumn, userID, 'Failed to get username. Press to retry', getAndAddUsername);
return;
}
if (res.response.userName === userID) {
return;
}
usernameColumn.replaceWith(createTextAreaColumnElement(res.response.userName, 'Username'));
},
onerror: () => {
console.warn('Couldn\'t get username due to an error');
appendRetryButtonToColumn(usernameColumn, userID, 'Failed to get username. Press to retry', getAndAddUsername);
},
ontimeout: () => {
console.warn('Couldn\'t get username due to timeout');
appendRetryButtonToColumn(usernameColumn, userID, 'Failed to get username. Press to retry', getAndAddUsername);
}
});
}
/**
* Appends a retry button the provided element.
* @param {HTMLTableCellElement} columnEl The element we want to append the retry button to
* @param {string} userID The userID of the user we want to check
* @param {string} btnMsg The message we want to display for the button on hover
* @param {function} functionToCallOnRetry The function to call on retry. The function will be called with columnEl and userID as parameters
*/
function appendRetryButtonToColumn(columnEl, userID, btnMsg, functionToCallOnRetry) {
let retryBtn = createSpan(btnMsg, ' ↻');
retryBtn.addEventListener('click', () => {
columnEl.removeChild(retryBtn);
functionToCallOnRetry(columnEl, userID);
});
retryBtn.style.cursor = 'pointer';
columnEl.appendChild(retryBtn);
}
/**
* Fixes/adds the "even" "odd" classes of the table's rows.
* Also Adds display:none to columns that are hidden with the Hide Columns userscript
* @param {HTMLTableSectionElement} tbody Tbody element of the table
*/
function fixRows(tbody) {
[...tbody.children].forEach((row, i) => {
row.className = i % 2 ? 'odd' : 'even';
const columnHeaders = [...tbody.parentElement.querySelectorAll('thead th')];
columnHeaders.forEach((header, j) => {
if (header.style.display === 'none') {
row.children[j].style.display = 'none';
}
});
});
}
/**
* Adds the refresh button to the page.
*/
function addRefreshButton() {
const btn = document.createElement('button');
btn.className = 'btn btn-primary';
btn.innerText = 'Force Refresh';
btn.title = 'You can also press the "R" key on your keyboard';
btn.onclick = () => {
const videoID = checkPageVideoID();
if (!videoID) return;
getAndUpdateSegments(videoID);
getAndUpdateLocks(videoID);
};
const referenceElArray = document.getElementsByClassName('col-auto');
const referenceEl = referenceElArray[referenceElArray.length - 2];
const parentEl = referenceEl.parentElement;
const copyEl = referenceEl.cloneNode();
copyEl.appendChild(btn);
parentEl.appendChild(copyEl);
}
/**
* Gets the ID of the video on the page and returns it if found
* @returns {null | string}
*/
function checkPageVideoID() {
const pathName = location.pathname.slice(1, -1).split('/');
let videoID;
if (pathName[0] === 'uuid') {
const url = new URL(document.getElementsByClassName('float-end ms-1')[0]?.href ?? 'https://www.example.com');
videoID = url.pathname.slice(1, -1).split('/')[1];
if (videoID?.length === 0) {
window.alert('Could not get the video ID');
return null;
}
} else {
videoID = pathName[1];
}
return videoID;
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment