- sb.ltn.fi.clickableellipsisnavigation.user.js
Makes it so clicking the ellipsis in the page navigation will show a prompt that asks what page you want to navigate to.
Install - sb.ltn.fi.copyuserid.user.js
Adds a button on the userID page to copy that userID
Install - sb.ltn.fi.copyvideopagelink.user.js
Makes the "copy video ID" (✂) button copy the video's SBB page's link to clipboard instead of the video ID.
Install - sb.ltn.fi.forceupdate.user.js
Fetches the segments and updated info for the video, so that you don't have to wait for the video page to update.
Note: You can also press the 'R' key on your keyboard to force a refresh
Install - sb.ltn.fi.hidecolumns.user.js
Hide any column you want on sb.ltn.fi
Install - sb.ltn.fi.pagenavigationabovetable.user.js
Duplicates the page navigation element and puts it above the table so you don't have to scroll down to change the page.
Note: To have the clickable ellipsis navigation script work for this page navigation element as well, this script needs to come before it in the Installed Userscripts section on Tampermonkey. Sort by "#" and drag this script above the "Clickable ellipsis navigation for sb.ltn.fi" script.
Install - sb.ltn.fi.videotitles.user.js
Replaces the video ID with the video title in the 'Video ID' column
Install - sb.ltn.fi.vipinterface.user.js
Adds option to lock categories and vote on segments.
Original script by Deedit. Now being Upkept and updated by TheJzoli.
Install
Last active
June 18, 2023 20:23
-
-
Save TheJzoli/8a4cd979d433b7359cdf61c238bc0181 to your computer and use it in GitHub Desktop.
Userscripts for sb.ltn.fi
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Copy UserID button for sb.ltn.fi | |
// @namespace sb.ltn.fi.copy.userid | |
// @version 1.0.3 | |
// @description Adds a button on the userID page to copy that userID | |
// @author TheJzoli | |
// @match https://sb.ltn.fi/userid/* | |
// @grant GM_setClipboard | |
// ==/UserScript== | |
(function() { | |
'use strict'; | |
const btn = document.createElement('button'); | |
btn.className = 'btn btn-primary'; | |
btn.style.margin = '20px'; | |
btn.innerText = 'Copy userID'; | |
const userID = location.pathname.split('/')[2]; | |
btn.onclick = () => GM_setClipboard(userID); | |
const element = document.getElementsByClassName('list-group-horizontal')[0]; | |
if (element) { | |
element.appendChild(btn); | |
} | |
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Copy SBB video page link on sb.ltn.fi | |
// @namespace sb.ltn.fi.copy.video.page.link | |
// @version 1.0.3 | |
// @description Makes the "copy video ID" button copy the video's SBB page's link to clipboard instead of the video ID. | |
// @author TheJzoli | |
// @match https://sb.ltn.fi/* | |
// ==/UserScript== | |
(function() { | |
'use strict'; | |
[...document.querySelectorAll('table')].forEach(table => { | |
const headers = [...table.querySelectorAll('thead th')].map(item => item.textContent.trim()); | |
if (headers.includes('VideoID') || headers.includes('Video')) { | |
const columnIndex = headers.includes('VideoID') ? headers.indexOf('VideoID') : headers.indexOf('Video'); | |
const rows = [...table.querySelectorAll('tbody tr')]; | |
rows.forEach(row => { | |
const href = row.children[columnIndex].firstElementChild.href; | |
const button = row.children[columnIndex].getElementsByTagName('button')[0]; | |
button.dataset.value = href; | |
}); | |
} | |
}); | |
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Force-update video page for sb.ltn.fi | |
// @namespace sb.ltn.fi.force.update | |
// @version 1.1.16 | |
// @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/TheJzoli/8a4cd979d433b7359cdf61c238bc0181/raw/sb.ltn.fi.forceupdate.user.js | |
// @downloadURL https://gist.github.com/TheJzoli/8a4cd979d433b7359cdf61c238bc0181/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]; | |
// Ways to have user refresh via a button or the R key | |
addRefreshButton(); | |
document.addEventListener('keyup', (e) => { | |
if (exclude.indexOf(e.target.tagName.toLowerCase()) === -1 && e.code === 'KeyR') { | |
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; | |
} | |
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Hide columns on sb.ltn.fi | |
// @namespace sb.ltn.fi.hide.columns | |
// @version 1.1.10 | |
// @description Hide any column you want on sb.ltn.fi | |
// @author TheJzoli | |
// @match https://sb.ltn.fi/* | |
// @grant GM_getValue | |
// @grant GM_setValue | |
// @grant GM_deleteValue | |
// ==/UserScript== | |
(function() { | |
'use strict'; | |
// Create settings button and menu. | |
const navBar = [...document.getElementsByClassName('navbar')][0].lastElementChild; | |
const buttonDiv = document.createElement('div'); | |
buttonDiv.style = 'position:relative;'; | |
const settingsBtn = document.createElement('button'); | |
settingsBtn.style = 'font-size:1.5em;background-color:transparent;border-style:none;transition-duration:500ms;transform-origin:50%55%;'; | |
settingsBtn.innerText = '⚙️'; | |
const settingsMenuEl = document.createElement('div'); | |
settingsMenuEl.classList.add('bg-light'); | |
settingsMenuEl.style = 'position:absolute;z-index:999;top:51px;left:50%;transform:translateX(-50%);padding:10px;border-radius:5px;box-shadow:4px 5px 5px black;white-space:nowrap;display:none;'; | |
settingsMenuEl.innerHTML = '<b>Column visibility:</b><br>'; | |
settingsBtn.onclick = () => { | |
settingsBtn.style.transform = (settingsMenuEl.style.display) ? 'rotate(180deg)' : 'rotate(-180deg)'; | |
settingsMenuEl.style.display = (settingsMenuEl.style.display) ? null : 'none'; | |
settingsBtn.blur(); | |
moveElementIntoViewport(settingsMenuEl); | |
} | |
buttonDiv.appendChild(settingsBtn); | |
buttonDiv.appendChild(settingsMenuEl); | |
navBar.insertBefore(buttonDiv, navBar.lastElementChild); | |
window.addEventListener('resize', () => { moveElementIntoViewport(settingsMenuEl) }); | |
const observableEl = document.getElementById('navbarNav'); | |
if (observableEl) { | |
const observer = new MutationObserver(() => { moveElementIntoViewport(settingsMenuEl) }); | |
observer.observe(observableEl, { | |
attributes: true, | |
attributeFilter: ['class'] | |
}); | |
} | |
[...document.querySelectorAll('table')].forEach(table => { | |
const headersText = [...table.querySelectorAll('thead th')].map(item => item.textContent.trim()); | |
getColumnData(headersText).then(columnsObj => { | |
const rows = [...table.querySelectorAll('tbody tr')]; | |
const headers = [...table.querySelectorAll('thead th')]; | |
if (columnsObj.special) { | |
combineShadowhiddenAndHiddenColumns(headersText, rows, headers, table); | |
} | |
// long forEach loop starts | |
headersText.forEach((headerText, i) => { | |
// Hide or show columns based on previously saved data. | |
if (columnsObj[headerText] == false) { | |
headers[i].style.display = 'none'; | |
rows.forEach(row => { | |
row.children[i].style.display = 'none'; | |
}); | |
} else if (columnsObj[headerText] == true) { | |
headers[i].style.display = null; | |
rows.forEach(row => { | |
row.children[i].style.display = null; | |
}); | |
} | |
// Populate settings menu's checkbox options based on previously saved data. | |
const checkboxInputEl = document.createElement('input'); | |
checkboxInputEl.setAttribute('type', 'checkbox'); | |
checkboxInputEl.setAttribute('id', `checkbox${i+1}`); | |
checkboxInputEl.checked = columnsObj[headerText]; | |
checkboxInputEl.style.marginRight = '10px'; | |
const labelEl = document.createElement('label'); | |
labelEl.setAttribute('for', `checkbox${i+1}`); | |
labelEl.innerText = headerText; | |
const brEl = document.createElement('br'); | |
settingsMenuEl.appendChild(checkboxInputEl); | |
settingsMenuEl.appendChild(labelEl); | |
settingsMenuEl.appendChild(brEl); | |
}); | |
// long forEach loop ends | |
// Create save button and its functionality. | |
const saveBtn = document.createElement('button'); | |
saveBtn.className = 'btn btn-primary'; | |
saveBtn.innerText = 'Save'; | |
saveBtn.onclick = async () => { | |
saveBtn.disabled = true; | |
const newColumnsObj = {}; | |
const specialCheckboxInputEl = document.getElementById('checkboxSpecial'); | |
newColumnsObj.special = specialCheckboxInputEl.checked; | |
const upToDateRows = [...table.querySelectorAll('tbody tr')]; | |
if (newColumnsObj.special) { | |
combineShadowhiddenAndHiddenColumns(headersText, upToDateRows, headers, table); | |
} else { | |
separateShadowhiddenAndHiddenColumns(upToDateRows, table); | |
} | |
headersText.forEach((headerText, i) => { | |
const checkboxInputEl = document.getElementById(`checkbox${i+1}`); | |
newColumnsObj[headerText] = checkboxInputEl.checked; | |
if (newColumnsObj[headerText] == false) { | |
headers[i].style.display = 'none'; | |
upToDateRows.forEach(row => { | |
row.children[i].style.display = 'none'; | |
}); | |
} else if (newColumnsObj[headerText] == true) { | |
headers[i].style.display = null; | |
upToDateRows.forEach(row => { | |
row.children[i].style.display = null; | |
}); | |
} | |
}); | |
await GM_setValue('columns', JSON.stringify(newColumnsObj)); | |
saveBtn.disabled = false; | |
} | |
settingsMenuEl.appendChild(saveBtn); | |
// Create checkbox that combines the shadowhidden and hidden columns. | |
const specialCheckboxInputEl = document.createElement('input'); | |
specialCheckboxInputEl.setAttribute('type', 'checkbox'); | |
specialCheckboxInputEl.setAttribute('id', 'checkboxSpecial'); | |
specialCheckboxInputEl.checked = columnsObj.special; | |
specialCheckboxInputEl.style.marginRight = '10px'; | |
specialCheckboxInputEl.style.marginLeft = '10px'; | |
const specialLabelEl = document.createElement('label'); | |
specialLabelEl.setAttribute('for', 'checkboxSpecial'); | |
specialLabelEl.style.marginBottom = '0'; | |
specialLabelEl.style.verticalAlign = 'middle'; | |
specialLabelEl.innerHTML = 'Combine Shadowhidden<br>and Hidden columns'; | |
const shadowhiddenIndex = headersText.indexOf('Shadowhidden'); | |
const shadowhiddenInputEl = document.getElementById(`checkbox${shadowhiddenIndex+1}`); | |
const hiddenIndex = headersText.indexOf('Hidden'); | |
const hiddenInputEl = document.getElementById(`checkbox${hiddenIndex+1}`); | |
if (specialCheckboxInputEl.checked) { | |
shadowhiddenInputEl.disabled = true; | |
hiddenInputEl.disabled = true; | |
} | |
specialCheckboxInputEl.onchange = () => { | |
if (specialCheckboxInputEl.checked) { | |
shadowhiddenInputEl.checked = false; | |
shadowhiddenInputEl.disabled = true; | |
hiddenInputEl.checked = false; | |
hiddenInputEl.disabled = true; | |
} else { | |
shadowhiddenInputEl.disabled = false; | |
shadowhiddenInputEl.checked = true; | |
hiddenInputEl.disabled = false; | |
hiddenInputEl.checked = true; | |
} | |
} | |
settingsMenuEl.appendChild(specialCheckboxInputEl); | |
settingsMenuEl.appendChild(specialLabelEl); | |
}).catch(error => console.warn(`${error}`)); | |
}); | |
})(); | |
async function getColumnData(headersText) { | |
//await GM_deleteValue('columns'); Uncomment this and load the page, if you want to delete the data you have saved in browser. | |
const columns = JSON.parse(await GM_getValue('columns', '{}')); | |
if (!columns) { | |
console.warn('Error! JSON.parse failed. Saved column data is invalid.'); | |
columns = {}; | |
} | |
headersText.forEach(headerText => { | |
const headerValue = columns[headerText]; | |
if (headerValue == undefined) { | |
columns[headerText] = true; | |
} | |
}); | |
columns.special = (columns.special == undefined) ? false : columns.special; | |
return columns; | |
} | |
function combineShadowhiddenAndHiddenColumns(headersText, rows, headers, table) { | |
const newHeadersText = [...table.querySelectorAll('thead th')].map(item => item.textContent.trim()); | |
if (newHeadersText.includes('S/H')) { | |
return; | |
} | |
const shadowhiddenValuesArray = []; | |
headersText.forEach((headerText, i) => { | |
if (headerText == 'Shadowhidden') { | |
rows.forEach(row => { | |
if (row.children[i].innerText == '—') { | |
shadowhiddenValuesArray.push(false); | |
} else { | |
shadowhiddenValuesArray.push(true); | |
}; | |
}); | |
} | |
}); | |
headersText.forEach((headerText, i) => { | |
if (headerText == 'Hidden') { | |
rows.forEach((row, j) => { | |
if (row.children[i].innerText !== '—') { | |
shadowhiddenValuesArray[j] = true; | |
} | |
}); | |
} | |
}); | |
const theadTrEl = headers[0].parentNode; | |
let newHeader; | |
headers.forEach(header => { | |
if (header.textContent.trim() === 'Hidden') { | |
newHeader = header.cloneNode(true); | |
} | |
}); | |
if (headers[headersText.indexOf('VideoID')]?.firstElementChild === null) { // We are on the main page | |
newHeader.innerText = 'S/H'; | |
newHeader.title = 'Shadowhidden/Hidden'; | |
} else { | |
newHeader.lastElementChild.innerText = 'S/H'; | |
newHeader.lastElementChild.title = 'Shadowhidden/Hidden'; | |
} | |
newHeader.style.display = null; | |
theadTrEl.appendChild(newHeader); | |
rows.forEach((row, i) => { | |
const newRowEl = document.createElement('td'); | |
newRowEl.innerText = (shadowhiddenValuesArray[i]) ? '❌' : '—'; | |
row.appendChild(newRowEl); | |
}); | |
} | |
function separateShadowhiddenAndHiddenColumns(rows, table) { | |
const newHeadersText = [...table.querySelectorAll('thead th')].map(item => item.textContent.trim()); | |
if (!newHeadersText.includes('S/H')) { | |
return; | |
} | |
const thead = [...table.querySelectorAll('thead')][0]; | |
thead.lastElementChild.lastElementChild.remove(); | |
rows.forEach(row => { | |
row.lastElementChild.remove(); | |
}); | |
} | |
function moveElementIntoViewport(element) { | |
const documentWidth = document.documentElement.clientWidth; | |
const rect = element.getBoundingClientRect(); | |
if (documentWidth < element.offsetWidth) { | |
return; | |
} | |
if (rect.left < 15 && rect.left !== 0) { | |
element.style.transform = null; | |
const rectNew = element.getBoundingClientRect(); | |
element.style.transform = `translateX(-${rectNew.left - 15}px)`; | |
} else if (rect.left >= 16 && rect.right <= (documentWidth - 16) && rect.left !== 0) { | |
element.style.transform = 'translateX(-50%)'; | |
} else if (rect.right > documentWidth) { | |
element.style.transform = 'translateX(-50%)'; | |
const rectNew = element.getBoundingClientRect(); | |
if (rectNew.right > documentWidth - 15) { | |
element.style.transform = null; | |
const rectNewer = element.getBoundingClientRect(); | |
element.style.transform = `translateX(-${rectNewer.right - (documentWidth - 15)}px)`; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Video Titles for sb.ltn.fi | |
// @namespace sb.ltn.fi.video.titles | |
// @version 1.2.12 | |
// @description Replaces the video ID with the video title in the 'VideoID' column. | |
// @author TheJzoli | |
// @match https://sb.ltn.fi/* | |
// @grant GM_xmlhttpRequest | |
// @grant GM_addStyle | |
// @connect sponsor.ajay.app | |
// @connect invidious.snopyta.org | |
// @connect invidious.namazso.eu | |
// @connect invidious.weblibre.org | |
// @connect vid.puffyan.us | |
// @connect invidious.flokinet.to | |
// @connect y.com.sb | |
// @connect invidious.tiekoetter.com | |
// @connect yt.artemislena.eu | |
// @connect inv.riverside.rocks | |
// @connect invidio.xamh.de | |
// ==/UserScript== | |
// In case some of these don't work anymore, | |
// replace them with ones from here: https://docs.invidious.io/instances/ | |
// Remember to add a @connect for the new url. | |
// I'll update the list every now and then when updating the userscript. | |
const URLS = ['https://sponsor.ajay.app/api/youtubeApiProxy?key=8NpFUCMr2Gq4cy4UrUJPBfGBbRQudhJ8zzex8Gq44RYDywLt3UtbbfDap3KPDbcS&videoID=', | |
'https://invidious.snopyta.org/api/v1/videos/', | |
'https://invidious.namazso.eu/api/v1/videos/', | |
'https://sponsor.ajay.app/api/youtubeApiProxy?key=8NpFUCMr2Gq4cy4UrUJPBfGBbRQudhJ8zzex8Gq44RYDywLt3UtbbfDap3KPDbcS&videoID=', | |
'https://vid.puffyan.us/api/v1/videos/', | |
'https://invidio.xamh.de/api/v1/videos/', | |
'https://sponsor.ajay.app/api/youtubeApiProxy?key=8NpFUCMr2Gq4cy4UrUJPBfGBbRQudhJ8zzex8Gq44RYDywLt3UtbbfDap3KPDbcS&videoID=', | |
'https://inv.riverside.rocks/api/v1/videos/', | |
'https://invidious.weblibre.org/api/v1/videos/', | |
'https://sponsor.ajay.app/api/youtubeApiProxy?key=8NpFUCMr2Gq4cy4UrUJPBfGBbRQudhJ8zzex8Gq44RYDywLt3UtbbfDap3KPDbcS&videoID=', | |
'https://y.com.sb/api/v1/videos/', | |
'https://invidious.flokinet.to/api/v1/videos/', | |
'https://sponsor.ajay.app/api/youtubeApiProxy?key=8NpFUCMr2Gq4cy4UrUJPBfGBbRQudhJ8zzex8Gq44RYDywLt3UtbbfDap3KPDbcS&videoID=', | |
'https://invidious.tiekoetter.com/api/v1/videos/', | |
'https://yt.artemislena.eu/api/v1/videos/']; | |
const URLS_LENGTH = URLS.length; | |
// Change this if you change the frequency that sponsor.ajay.app is called. 0 if not at all. | |
const AJAY_URL_EVERY_X_CALL = 3; | |
const UrlWorks = []; | |
const videoIdAndRowElementObj = {}; | |
(function() { | |
'use strict'; | |
URLS.forEach(() => {UrlWorks.push(true)}); | |
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')].forEach(table => { | |
const headersArray = [...table.querySelectorAll('thead th')]; | |
const headers = headersArray.map(item => item.textContent.trim()); | |
if (headers.includes('VideoID')) { | |
const columnIndex = headers.indexOf('VideoID'); | |
if (headersArray[columnIndex].firstElementChild === null) { // If null, we're on the frontpage. If not, videopage. | |
headersArray[columnIndex].innerText = "Video"; | |
} else { | |
headersArray[columnIndex].firstElementChild.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]; | |
} | |
}); | |
let index = 0; | |
for (const [key, value] of Object.entries(videoIdAndRowElementObj)) { | |
callApis(key, value, index); | |
if (index !== URLS_LENGTH - 1) { | |
index++; | |
} else { | |
index = 0; | |
} | |
} | |
} | |
}); | |
})(); | |
function callApis(videoID, videoIdElArray, index) { | |
// Every x call is to sponsor.ajay.app, which doesn't take the fields parameter. | |
let requestUrl = URLS[index].concat(videoID); | |
if (index % AJAY_URL_EVERY_X_CALL !== 0) { | |
requestUrl += '?fields=title'; | |
} | |
try { | |
GM_xmlhttpRequest({ | |
method: 'GET', | |
url: requestUrl, | |
responseType: 'json', | |
timeout: 10000, | |
onload: (responseObject) => { | |
if (responseObject?.status !== 200 && responseObject?.status !== 304) { | |
changeUrlWorksValue(index, false); | |
console.log(`${requestUrl} returned a status of ${responseObject?.status}\nUsing another instance...`); | |
// Try another random instance that works | |
retryWithDifferentUrl(videoID, videoIdElArray); | |
return; | |
} | |
changeUrlWorksValue(index, true); | |
// Inject the new name in place of the old video ID | |
if (responseObject?.response?.title) { | |
videoIdElArray.forEach(videoIdEl => { | |
videoIdEl.innerText = responseObject.response.title; | |
}); | |
} | |
removeLoadingIcons(videoIdElArray); | |
}, | |
onerror: () => { | |
changeUrlWorksValue(index, false); | |
console.log(`${requestUrl} doesn't exist anymore.\nUsing another instance...`); | |
retryWithDifferentUrl(videoID, videoIdElArray); | |
}, | |
ontimeout: () => { | |
changeUrlWorksValue(index, false); | |
console.log(`${requestUrl} timed out.\nUsing another instance...`); | |
retryWithDifferentUrl(videoID, videoIdElArray); | |
} | |
}); | |
} catch (error) { | |
console.error(error); | |
} | |
} | |
function changeUrlWorksValue(index, bool) { | |
if (index % AJAY_URL_EVERY_X_CALL !== 0) { | |
UrlWorks[index] = bool; | |
} else { | |
let i = Math.ceil(URLS_LENGTH / AJAY_URL_EVERY_X_CALL) - 1; | |
for (; i >= 0; i--) { | |
UrlWorks[AJAY_URL_EVERY_X_CALL * i] = bool; | |
} | |
} | |
} | |
function removeLoadingIcons(videoIdElArray) { | |
videoIdElArray.forEach(videoIdEl => { | |
videoIdEl.firstElementChild?.classList.remove('loading'); | |
}); | |
} | |
function retryWithDifferentUrl (videoID, videoIdElArray) { | |
const trueUrlsIndexes = []; | |
UrlWorks.forEach((bool, i) => { | |
if (bool) trueUrlsIndexes.push(i); | |
}); | |
// trueUrlsIndexes length is 0 if all are false | |
if (trueUrlsIndexes.length !== 0) { | |
const randomIndex = Math.floor(Math.random() * trueUrlsIndexes.length); | |
callApis(videoID, videoIdElArray, trueUrlsIndexes[randomIndex]); | |
} else { | |
removeLoadingIcons(videoIdElArray); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name SponsorBlock Vip Interface | |
// @namespace sb.ltn.fi.vipinterface | |
// @version 1.0.12 | |
// @description Adds option to lock categories and vote on segments. | |
// @author Deedit, TheJzoli | |
// @match https://sb.ltn.fi/* | |
// @connect sponsor.ajay.app | |
// @grant GM_xmlhttpRequest | |
// @grant GM_setValue | |
// @grant GM_getValue | |
// @updateURL https://gist.github.com/TheJzoli/8a4cd979d433b7359cdf61c238bc0181/raw/sb.ltn.fi.vipinterface.user.js | |
// @downloadURL https://gist.github.com/TheJzoli/8a4cd979d433b7359cdf61c238bc0181/raw/sb.ltn.fi.vipinterface.user.js | |
// ==/UserScript== | |
// Thanks Hanabishi, blabdude and TheJzoli for the help and Nanobyte for having the acces to the category types in his colorcode userscript already done | |
// !!!!Original script by Deedit!!!! Being upkept by TheJzoli. | |
(async function() { | |
'use strict'; | |
//consts | |
const userID = await GM_getValue("privateUserID", ""); | |
const categoryListUI = ['Sponsor', 'Promo', 'Interaction', 'Intro', 'Outro', 'Preview', 'Filler', 'Music', 'POI']; | |
const categoryListAPI = ['sponsor', 'selfpromo', 'interaction', 'intro', 'outro', 'preview', 'filler', 'music_offtopic', 'poi_highlight']; | |
let UUIDsaver = ''; | |
// create UI | |
createDropDownMenu(); | |
categoryChange(); | |
document.addEventListener('newSegments', () => { | |
categoryChange(); | |
}); | |
// Stuff for changing categories | |
function categoryChange(){ | |
createCategoryButtons(); | |
createCategorySelectorPopup(); | |
} | |
function createCategoryButtons() { | |
[...document.querySelectorAll('table')].forEach(table => { | |
const headers = [...table.querySelectorAll('thead th')].map(item => item.textContent.trim()); | |
if (headers.includes('Start') && headers.includes('End')) { | |
const columnIndex = headers.indexOf('Category'); | |
const rows = [...table.querySelectorAll('tbody tr')]; | |
rows.forEach((row, i) => { | |
// If this update is due to a newSegment event, we need to check for previous elements | |
const targetEl = row.children[columnIndex].lastElementChild; | |
if (targetEl && targetEl.hasAttribute('id')) { | |
targetEl.setAttribute('id', `lockChangeCategoryButton${i}`); | |
return; | |
} | |
const buttonEl = document.createElement('BUTTON'); | |
buttonEl.setAttribute('id', `lockChangeCategoryButton${i}`); | |
buttonEl.width = "100%"; | |
buttonEl.onclick = (event) => { | |
buttonEl.blur(); | |
const oldUUIDsaver = UUIDsaver; | |
UUIDsaver = row.children[headers.indexOf('UUID')].firstElementChild.value; | |
// Only disable the popup, when the same UUID was pressed twice | |
if (oldUUIDsaver == UUIDsaver) { | |
const popup = document.getElementById('changeCategorySelector'); | |
popup.style.display = (popup.style.display) ? null : 'none'; | |
popup.style.left = (event.clientX + 90).toString() + 'px'; | |
moveElementIntoViewport(popup); | |
} else { | |
const popup = document.getElementById('changeCategorySelector'); | |
popup.style.left = (event.clientX + 90).toString() + 'px'; | |
popup.style.display = null; | |
moveElementIntoViewport(popup); | |
} | |
} | |
//for normal use | |
if (targetEl === null){ | |
const categoryEl = row.children[columnIndex]; | |
const name = categoryEl.textContent.trim(); | |
categoryEl.textContent = ''; | |
buttonEl.innerText = name; | |
categoryEl.appendChild(buttonEl); | |
buttonEl.style.color = 'white'; | |
} else { // So that it works properly with colored categories | |
targetEl.parentNode.insertBefore(buttonEl, targetEl); | |
buttonEl.appendChild(targetEl); | |
} | |
}); | |
}}); | |
} | |
function createCategorySelectorPopup() { | |
const categorySelectorEl = document.createElement('div'); | |
//This is just, so that it is accessible via its ID | |
document.body.appendChild(categorySelectorEl); | |
categorySelectorEl.classList.add('bg-light'); | |
categorySelectorEl.style = 'position:fixed;z-index:999;top:30%;left:44%;padding:10px;border-radius:5px;box-shadow:4px 5px 5px black;white-space:nowrap;display:none;'; | |
categorySelectorEl.setAttribute('id', 'changeCategorySelector'); | |
// Category Buttons | |
const categoryListUICopy = [...categoryListUI]; | |
categoryListUICopy.splice(categoryListUI.indexOf('POI'), 1); | |
categoryListUICopy.forEach((name, i) => { | |
const labelEl = document.createTextNode(name); | |
const buttonEl = document.createElement('BUTTON'); | |
buttonEl.setAttribute('id', `lockSelectCategoryButton${i}`); | |
buttonEl.className = 'btn btn-secondary'; | |
buttonEl.style.width = '240px'; | |
buttonEl.style.margin = '2px 2px'; | |
buttonEl.style.padding = '0'; | |
buttonEl.onclick = () => { | |
buttonEl.blur(); | |
categorySelectorEl.style.display = 'none'; | |
const categoryListAPICopy = [...categoryListAPI]; | |
categoryListAPICopy.splice(categoryListAPI.indexOf('poi_highlight'), 1); | |
setCategory(UUIDsaver, categoryListAPICopy[i]); | |
} | |
buttonEl.appendChild(labelEl); | |
categorySelectorEl.appendChild(buttonEl); | |
categorySelectorEl.appendChild(document.createElement('br')); | |
}); | |
// Close button | |
const labelCloseEl = document.createTextNode('❌'); | |
const buttonCloseEl = document.createElement('BUTTON'); | |
buttonCloseEl.className = 'btn'; | |
buttonCloseEl.style.backgroundColor = 'black'; | |
buttonCloseEl.style.width = '240px'; | |
buttonCloseEl.style.margin = '2px 2px'; | |
buttonCloseEl.style.padding = '0'; | |
buttonCloseEl.setAttribute('id', `lockSelectCategoryCloseButton`); | |
buttonCloseEl.onclick = () => { | |
buttonCloseEl.blur(); | |
categorySelectorEl.style.display = 'none'; | |
} | |
buttonCloseEl.appendChild(labelCloseEl); | |
categorySelectorEl.appendChild(buttonCloseEl); | |
categorySelectorEl.appendChild(document.createElement('br')); | |
// Vote buttons | |
const voteNames = ['Upvote','Downvote','Undo vote']; | |
const voteCodes = [1,0,20]; | |
voteNames.forEach((name, i) => { | |
const labelEl = document.createTextNode(name); | |
const buttonEl = document.createElement('BUTTON'); | |
buttonEl.setAttribute('id', `lockSelectCategoryButton${i}`); | |
buttonEl.className = 'btn btn-secondary'; | |
buttonEl.style.width = '240px'; | |
buttonEl.style.margin = '2px 2px'; | |
buttonEl.style.padding = '0'; | |
buttonEl.onclick = () => { | |
buttonEl.blur(); | |
categorySelectorEl.style.display = 'none'; | |
voteOnSegment(UUIDsaver, voteCodes[i]); | |
} | |
buttonEl.appendChild(labelEl); | |
categorySelectorEl.appendChild(buttonEl); | |
categorySelectorEl.appendChild(document.createElement('br')); | |
}); | |
window.addEventListener('resize', () => { moveElementIntoViewport(categorySelectorEl) }); | |
} | |
function setCategory(UUID, category){ | |
//console.log('https://sponsor.ajay.app/api/voteOnSponsorTime?UUID=' + UUID + '&userID=' + userID + '&category=' + category); | |
document.body.style.cursor = "progress"; | |
GM_xmlhttpRequest({ | |
method: 'GET', | |
url: 'https://sponsor.ajay.app/api/voteOnSponsorTime?UUID=' + UUID + '&userID=' + userID + '&category=' + category, | |
timeout: 10000, | |
onload: (res) => { | |
document.body.style.cursor = null; | |
if (!res) { | |
window.alert("Response object was undefined for some reason. Category was most likely not changed!"); | |
return; | |
} | |
if (res.status !== 200 && res.status !== 304) { | |
window.alert(`Could not change category! Server responded with status ${res.status}: ${res.statusText ?? ""}`); | |
} | |
}, | |
onerror: () => { | |
document.body.style.cursor = null; | |
window.alert("Unknown error occurred and category wasn't changed!"); | |
}, | |
ontimeout: () => { | |
document.body.style.cursor = null; | |
window.alert("Request timed out and category may not have been changed!"); | |
} | |
}); | |
} | |
function voteOnSegment(UUID, voteType){ | |
document.body.style.cursor = "progress"; | |
GM_xmlhttpRequest({ | |
method: 'GET', | |
url: 'https://sponsor.ajay.app/api/voteOnSponsorTime?UUID=' + UUID + '&userID=' + userID + '&type=' + voteType, | |
timeout: 10000, | |
onload: (res) => { | |
document.body.style.cursor = null; | |
if (!res) { | |
window.alert("Response object was undefined for some reason. Vote most likely didn't go through!"); | |
return; | |
} | |
if (res.status !== 200 && res.status !== 304) { | |
window.alert(`Vote didn't go through! Server responded with status ${res.status}: ${res.statusText ?? ""}`); | |
} | |
}, | |
onerror: () => { | |
document.body.style.cursor = null; | |
window.alert("Unknown error occurred and segment wasn't voted!"); | |
}, | |
ontimeout: () => { | |
document.body.style.cursor = null; | |
window.alert("Request timed out and segment may not have been voted!"); | |
} | |
}); | |
} | |
// Stuff for categorie locks/ purge / clear cache | |
function createDropDownMenu() { | |
// Create Lock-symbol button and "dropdown"-menu. | |
const navBar = [...document.getElementsByClassName('navbar')][0].lastElementChild; | |
const lockButtonDiv = document.createElement('div'); | |
lockButtonDiv.style = 'position:relative;'; | |
const lockBtn = document.createElement('button'); | |
lockBtn.style = 'font-size:1.5em;background-color:transparent;border-style:none;'; | |
lockBtn.innerText = '🔒'; | |
const lockMenuEl = document.createElement('div'); | |
lockMenuEl.classList.add('bg-light'); | |
lockMenuEl.style = 'position:absolute;z-index:999;top:51px;left:50%;transform:translateX(-50%);padding:10px;border-radius:5px;box-shadow:4px 5px 5px black;white-space:nowrap;display:none;'; | |
lockBtn.onclick = () => { | |
//if (lockMenuEl.style.display === 'none') getReasons(); | |
// null is visible, none is invisible | |
const menuVisibility = (lockMenuEl.style.display) ? null : 'none'; | |
lockMenuEl.style.display = menuVisibility; | |
lockBtn.innerText = menuVisibility ? '🔒' : '🔓'; | |
lockBtn.blur(); | |
moveElementIntoViewport(lockMenuEl); | |
} | |
lockButtonDiv.appendChild(lockBtn); | |
lockButtonDiv.appendChild(lockMenuEl); | |
navBar.insertBefore(lockButtonDiv, navBar.lastElementChild); | |
// Create Dropdown elements | |
const lockCategorieRegex = new RegExp('https:\/\/sb\.ltn\.fi\/video\/*'); | |
if (lockCategorieRegex.test(document.URL)) { | |
const videoID = document.URL.split('/')[4]; | |
lockMenuEl.style.transform = 'translateX(-50%)'; | |
createLockButton(lockMenuEl, videoID); | |
createCheckboxButton(lockMenuEl); | |
createLockTable(lockMenuEl); | |
createUnlockButton(lockMenuEl, videoID); | |
createSetUserIDButton(lockMenuEl); | |
lockMenuEl.appendChild(document.createElement('br')); | |
//createClearCacheButton(lockMenuEl, videoID); | |
//createPurgeSegmentsButton(lockMenuEl, videoID); | |
} else { | |
createSetUserIDButton(lockMenuEl); | |
lockMenuEl.style.transform = 'translateX(-50%)'; | |
} | |
window.addEventListener('resize', () => { moveElementIntoViewport(lockMenuEl) }); | |
const observableEl = document.getElementById('navbarNav'); | |
if (observableEl) { | |
const observer = new MutationObserver(() => { moveElementIntoViewport(lockMenuEl) }); | |
observer.observe(observableEl, { | |
attributes: true, | |
attributeFilter: ['class'] | |
}); | |
} | |
} | |
function createLockButton(lockMenuEl, videoID) { | |
// Create button that triggers category locking | |
const buttonEl = document.createElement('BUTTON'); | |
buttonEl.setAttribute('id', 'lockLockCategoriesButton'); | |
buttonEl.className = 'btn btn-secondary'; | |
buttonEl.style.width = '240px'; | |
buttonEl.style.margin = '2px 2px'; | |
buttonEl.onclick = () => { | |
buttonEl.blur(); | |
categoryListAPI.forEach((name,i) => { | |
if (document.getElementById(`lockCheckbox${i}`).checked) { | |
const lockReason = document.getElementById(`lockReason${i}`).value; | |
setReasons(name, lockReason, videoID); | |
} | |
}); | |
} | |
const buttonText = document.createTextNode("Lock"); | |
buttonEl.appendChild(buttonText); | |
lockMenuEl.appendChild(buttonEl); | |
} | |
function createCheckboxButton(lockMenuEl) { | |
// Create button that selects all checkboxes | |
const buttonEl = document.createElement('BUTTON'); | |
buttonEl.setAttribute('id', 'lockSelectAll'); | |
buttonEl.className = 'btn btn-secondary'; | |
buttonEl.style.width = '240px'; | |
buttonEl.style.margin = '2px 2px'; | |
buttonEl.onclick = () => { | |
// Check whether all are checked. | |
let allTrue = true; | |
categoryListUI.forEach((name, i) => { | |
if (name != 'POI' && name != 'Filler') { | |
allTrue = allTrue && document.getElementById(`lockCheckbox${i}`).checked; | |
} | |
}); | |
categoryListUI.forEach((name, i) => { | |
if (name != 'POI' && name != 'Filler') { | |
document.getElementById(`lockCheckbox${i}`).checked = (allTrue) ? false: true; | |
} | |
}); | |
buttonEl.blur(); | |
} | |
const buttonText = document.createTextNode("Select all"); | |
buttonEl.appendChild(buttonText); | |
lockMenuEl.appendChild(buttonEl); | |
} | |
function createLockTable(lockMenuEl) { | |
// Create table that is inside the "dropdown"-menu | |
const tableBodyEl = document.createElement("TBODY"); | |
const tableEl = document.createElement("TABLE"); | |
lockMenuEl.appendChild(tableEl); | |
tableEl.appendChild(tableBodyEl); | |
// Create a table with checkboxes inside of labels and textareas | |
categoryListUI.forEach((name, i) => { | |
const tableRowEl = document.createElement("TR"); | |
const tableLabelEl = document.createElement("TD"); | |
tableLabelEl.style.userSelect = "none"; | |
const tableReasonEl = document.createElement("TD"); | |
const checkboxEl = document.createElement('input'); | |
checkboxEl.setAttribute('type', 'checkbox'); | |
checkboxEl.setAttribute('id', `lockCheckbox${i}`); | |
checkboxEl.style.marginLeft = '5px'; | |
const labelEl = document.createElement('label'); | |
labelEl.innerText = name; | |
labelEl.style.height = "75px"; | |
labelEl.style.verticalAlign = "middle"; | |
const reasonTextareaEl = document.createElement('TEXTAREA'); | |
const reasonEl = document.createTextNode(''); | |
reasonTextareaEl.setAttribute('id', `lockReason${i}`); | |
reasonTextareaEl.style.height = '75px'; | |
reasonTextareaEl.style.width = '400px'; | |
reasonTextareaEl.className = 'form-control'; | |
reasonTextareaEl.appendChild(reasonEl); | |
labelEl.appendChild(checkboxEl); | |
tableLabelEl.appendChild(labelEl); | |
tableReasonEl.appendChild(reasonTextareaEl); | |
tableRowEl.appendChild(tableLabelEl); | |
tableRowEl.appendChild(tableReasonEl); | |
tableBodyEl.appendChild(tableRowEl); | |
}); | |
} | |
function createUnlockButton(lockMenuEl, videoID) { | |
// Create button that triggers category UNlocking | |
const buttonEl = document.createElement('BUTTON'); | |
buttonEl.setAttribute('id', 'lockUnlockCategoriesButton'); | |
buttonEl.className = 'btn btn-secondary'; | |
buttonEl.style.width = '240px'; | |
buttonEl.style.margin = '2px 2px'; | |
buttonEl.onclick = () => { | |
buttonEl.blur(); | |
const names = []; | |
categoryListAPI.forEach((name,i) => { | |
if (document.getElementById(`lockCheckbox${i}`).checked) { | |
names.push(name); | |
} | |
}); | |
unSetReasons(names, videoID); | |
} | |
const buttonText = document.createTextNode("Unlock"); | |
buttonEl.appendChild(buttonText); | |
lockMenuEl.appendChild(buttonEl); | |
} | |
function createSetUserIDButton(lockMenuEl) { | |
// Button for the UserID | |
const buttonEl = document.createElement('BUTTON'); | |
buttonEl.className = 'btn btn-secondary'; | |
buttonEl.style.width = '240px'; | |
buttonEl.style.margin = '2px 2px'; | |
buttonEl.onclick = () => { | |
buttonEl.blur(); | |
let tempUserID = prompt('Set private UserID') | |
if (tempUserID !== null) GM_setValue("privateUserID", tempUserID); | |
} | |
const buttonText = document.createTextNode("Set UserID"); | |
buttonEl.appendChild(buttonText); | |
lockMenuEl.appendChild(buttonEl); | |
} | |
function createClearCacheButton(lockMenuEl, videoID) { | |
// Create button that triggers category UNlocking | |
const buttonEl = document.createElement('BUTTON'); | |
buttonEl.setAttribute('id', 'lockClearCacheButton'); | |
buttonEl.className = 'btn btn-secondary'; | |
buttonEl.style.width = '240px'; | |
buttonEl.style.margin = '2px 2px'; | |
buttonEl.onclick = () => { | |
buttonEl.blur(); | |
clearCache(videoID); | |
} | |
const buttonText = document.createTextNode("Clear cache"); | |
buttonEl.appendChild(buttonText); | |
lockMenuEl.appendChild(buttonEl); | |
} | |
function createPurgeSegmentsButton(lockMenuEl, videoID) { | |
// Create button that triggers category UNlocking | |
const buttonEl = document.createElement('BUTTON'); | |
buttonEl.setAttribute('id', 'lockPurgeSegmentsButton'); | |
buttonEl.className = 'btn btn-secondary'; | |
buttonEl.style.width = '240px'; | |
buttonEl.style.margin = '2px 2px'; | |
buttonEl.onclick = () => { | |
buttonEl.blur(); | |
if (prompt('Write "yes" to confirm.') == 'yes') { | |
console.log('purged'); | |
clearCache(videoID); | |
} | |
} | |
const buttonText = document.createTextNode("Purge segments"); | |
buttonEl.appendChild(buttonText); | |
lockMenuEl.appendChild(buttonEl); | |
} | |
function setReasons (category, lockReason, videoID){ | |
// Set all 8 categories lock reasons and locks, or it unlocks if checkbox isn't set | |
document.body.style.cursor = "progress"; | |
document.getElementById("lockLockCategoriesButton").style.cursor = "progress"; | |
GM_xmlhttpRequest({ | |
method: 'POST', | |
url: 'https://sponsor.ajay.app/api/lockCategories', | |
data: JSON.stringify({ | |
videoID: videoID, | |
userID: userID, | |
categories: [category], | |
reason: lockReason | |
}), | |
headers: { 'Content-Type': 'application/json' }, | |
timeout: 10000, | |
onload: (res) => { | |
document.body.style.cursor = null; | |
document.getElementById("lockLockCategoriesButton").style.cursor = null; | |
if (!res) { | |
window.alert(`Response object was undefined for some reason. ${category} was most likely not locked!`); | |
return; | |
} | |
if (res.status !== 200 && res.status !== 304) { | |
window.alert(`Could not lock ${category}! Server responded with status ${res.status}: ${res.statusText ?? ""}`); | |
} | |
}, | |
onerror: () => { | |
document.body.style.cursor = null; | |
document.getElementById("lockLockCategoriesButton").style.cursor = null; | |
window.alert(`Unknown error occurred and ${category} wasn't locked!`); | |
}, | |
ontimeout: () => { | |
document.body.style.cursor = null; | |
document.getElementById("lockLockCategoriesButton").style.cursor = null; | |
window.alert(`Request timed out and ${category} may not have been locked!`); | |
} | |
}); | |
} | |
function unSetReasons (categories, videoID){ | |
// Unlockes selectd categories | |
document.body.style.cursor = "progress"; | |
document.getElementById("lockUnlockCategoriesButton").style.cursor = "progress"; | |
GM_xmlhttpRequest({ | |
method: 'DELETE', | |
url: 'https://sponsor.ajay.app/api/lockCategories', | |
data: JSON.stringify({ | |
videoID: videoID, | |
userID: userID, | |
categories: categories | |
}), | |
headers: { 'Content-Type': 'application/json' }, | |
timeout: 10000, | |
onload: (res) => { | |
document.body.style.cursor = null; | |
document.getElementById("lockUnlockCategoriesButton").style.cursor = null; | |
if (!res) { | |
window.alert(`Response object was undefined for some reason. Categories were most likely not unlocked!`); | |
return; | |
} | |
if (res.status !== 200 && res.status !== 304) { | |
window.alert(`Could not unlock categories! Server responded with status ${res.status}: ${res.statusText ?? ""}`); | |
} | |
}, | |
onerror: () => { | |
document.body.style.cursor = null; | |
document.getElementById("lockUnlockCategoriesButton").style.cursor = null; | |
window.alert(`Unknown error occurred and categories weren't unlocked!`); | |
}, | |
ontimeout: () => { | |
document.body.style.cursor = null; | |
document.getElementById("lockUnlockCategoriesButton").style.cursor = null; | |
window.alert(`Request timed out and categories may not have been unlocked!`); | |
} | |
}); | |
} | |
function getReasons() { | |
// Calls API and fills reasons in textareas | |
GM_xmlhttpRequest({ | |
method: 'GET', | |
//url: `https://sponsor.ajay.app/api/lockCategories?videoID=${id}`, | |
url: `https://sponsor.ajay.app/api/lockCategories?videoID=qJeKZkK31JE`, | |
responseType: 'json', | |
onload: (res) => { | |
// https://wiki.greasespot.net/GM.xmlHttpRequest#Response_Object is what is returned | |
if(!res) return; | |
const d = document.createElement('div'); // TODO | |
let page = prompt(`Reason: ${res.status ?? ''}`); | |
}, | |
}); | |
} | |
function clearCache (videoID){ | |
document.body.style.cursor = "progress"; | |
document.getElementById("lockClearCacheButton").style.cursor = "progress"; | |
GM_xmlhttpRequest({ | |
method: 'POST', | |
url: 'https://sponsor.ajay.app/api/clearCache', | |
data: JSON.stringify({ | |
videoID: videoID, | |
userID: userID | |
}), | |
headers: { 'Content-Type': 'application/json' }, | |
timeout: 10000, | |
onload: (res) => { | |
document.body.style.cursor = null; | |
document.getElementById("lockClearCacheButton").style.cursor = null; | |
if (!res) { | |
window.alert(`Response object was undefined for some reason. Cache was most likely not cleared!`); | |
return; | |
} | |
if (res.status !== 200 && res.status !== 304) { | |
window.alert(`Could not clear cache! Server responded with status ${res.status}: ${res.statusText ?? ""}`); | |
} | |
}, | |
onerror: () => { | |
document.body.style.cursor = null; | |
document.getElementById("lockClearCacheButton").style.cursor = null; | |
window.alert(`Unknown error occurred and cache wasn't cleared!`); | |
}, | |
ontimeout: () => { | |
document.body.style.cursor = null; | |
document.getElementById("lockClearCacheButton").style.cursor = null; | |
window.alert(`Request timed out and cache may not have been cleared!`); | |
} | |
}); | |
} | |
function purgeSegments (videoID){ | |
document.body.style.cursor = "progress"; | |
document.getElementById("lockPurgeSegmentsButton").style.cursor = "progress"; | |
GM_xmlhttpRequest({ | |
method: 'POST', | |
url: 'https://sponsor.ajay.app/api/purgeAllSegments', | |
data: JSON.stringify({ | |
videoID: videoID, | |
userID: userID | |
}), | |
headers: { 'Content-Type': 'application/json' }, | |
timeout: 10000, | |
onload: (res) => { | |
document.body.style.cursor = null; | |
document.getElementById("lockPurgeSegmentsButton").style.cursor = null; | |
if (!res) { | |
window.alert(`Response object was undefined for some reason. Segments were most likely not purged!`); | |
return; | |
} | |
if (res.status !== 200 && res.status !== 304) { | |
window.alert(`Could not purge segments! Server responded with status ${res.status}: ${res.statusText ?? ""}`); | |
} | |
}, | |
onerror: () => { | |
document.body.style.cursor = null; | |
document.getElementById("lockPurgeSegmentsButton").style.cursor = null; | |
window.alert(`Unknown error occurred and segments weren't purged!`); | |
}, | |
ontimeout: () => { | |
document.body.style.cursor = null; | |
document.getElementById("lockPurgeSegmentsButton").style.cursor = null; | |
window.alert(`Request timed out and segments may not have been purged!`); | |
} | |
}); | |
} | |
function moveElementIntoViewport(element) { | |
const documentWidth = document.documentElement.clientWidth; | |
const rect = element.getBoundingClientRect(); | |
if (documentWidth < element.offsetWidth) { | |
return; | |
} | |
if (rect.left < 15 && rect.left !== 0) { | |
element.style.transform = null; | |
const rectNew = element.getBoundingClientRect(); | |
element.style.transform = `translateX(-${rectNew.left - 15}px)`; | |
} else if (rect.right > documentWidth - 15) { | |
element.style.transform = null; | |
const rectNew = element.getBoundingClientRect(); | |
element.style.transform = `translateX(-${rectNew.right - (documentWidth - 15)}px)`; | |
} else if (rect.left >= 16 && rect.right <= (documentWidth - 16) && rect.left !== 0) { | |
if (element.getAttribute('id') === 'changeCategorySelector') { | |
element.style.transform = null; | |
} else { | |
element.style.transform = 'translateX(-50%)'; | |
} | |
const rectNew = element.getBoundingClientRect(); | |
if (rectNew.right > documentWidth - 15) { | |
element.style.transform = `translateX(-${rectNew.right - (documentWidth - 15)}px)`; | |
} | |
} | |
} | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment