Skip to content

Instantly share code, notes, and snippets.

@TheJzoli
Last active Jan 14, 2022
Embed
What would you like to do?
Userscripts for sb.ltn.fi

Userscripts for sb.ltn.fi (by TheJzoli)

  • 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.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
// ==UserScript==
// @name Clickable ellipsis navigation for sb.ltn.fi
// @namespace sb.ltn.fi.clickable.ellipsis.navigation
// @version 1.0.1
// @description Makes it so clicking the ellipsis in the page navigation will show a prompt that asks what page you want to navigate to.
// @author TheJzoli
// @match https://sb.ltn.fi/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
[...document.getElementsByClassName('pagination')].forEach(paginationEl => {
const pageBtnTexts = [...paginationEl.querySelectorAll('li a')].map(item => item.textContent.trim());
const ellipsisBtns = [];
pageBtnTexts.forEach((text, i) => {
if (text == '...') {
ellipsisBtns.push([...paginationEl.querySelectorAll('li a')][i]);
}
});
ellipsisBtns.forEach(btn => {
btn.style.cursor = 'pointer';
btn.onclick = () => {
let currentPage = 1;
const searchParams = new URLSearchParams(location.search);
let possibleCurrentPage = searchParams.get('page');
if (possibleCurrentPage) {
currentPage = possibleCurrentPage;
}
let maxPage = pageBtnTexts[pageBtnTexts.length - 1];
if (!parseInt(maxPage)) {
maxPage = pageBtnTexts[pageBtnTexts.length - 2];
}
let page = prompt(`Jump to page: (1-${maxPage})`, currentPage);
if (parseInt(page)) {
searchParams.set('page', Math.min(maxPage, Math.max(1, page)));
document.location = location.origin + location.pathname + '?' + searchParams.toString();
}
}
});
});
})();
// ==UserScript==
// @name Copy UserID button for sb.ltn.fi
// @namespace sb.ltn.fi.copy.userid
// @version 1.0.2
// @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('mt-1')[0];
if (element) {
element.appendChild(btn);
}
})();
// ==UserScript==
// @name Hide columns on sb.ltn.fi
// @namespace sb.ltn.fi.hide.columns
// @version 1.1.3
// @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];
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;';
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 = () => {
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('navbarSupportedContent');
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;
if (newColumnsObj.special) {
combineShadowhiddenAndHiddenColumns(headersText, rows, headers, table);
} else {
separateShadowhiddenAndHiddenColumns(rows, 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';
rows.forEach(row => {
row.children[i].style.display = 'none';
});
} else if (newColumnsObj[headerText] == true) {
headers[i].style.display = null;
rows.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);
}
});
newHeader.lastElementChild.innerText = 'S/H';
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)`;
}
}
}
// ==UserScript==
// @name Page navigation above the table on sb.ltn.fi
// @namespace sb.ltn.fi.above.table.page.navigation
// @version 1.0.3
// @description Duplicates the page navigation element and puts it above the table so you don't have to scroll down to change the page.
// @author TheJzoli
// @match https://sb.ltn.fi/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
const navigationElClone = document.querySelector('[aria-label="Table navigation"]')?.cloneNode(true);
if (navigationElClone) {
navigationElClone.lastElementChild.style.marginBottom = '0';
const tableEl = document.getElementsByClassName('table')[0];
const tableContainerEl = tableEl.parentElement;
tableContainerEl.insertBefore(navigationElClone, tableEl);
}
})();
// ==UserScript==
// @name Video Titles for sb.ltn.fi
// @namespace sb.ltn.fi.video.titles
// @version 1.2.9
// @description Replaces the video ID with the video title in the 'Video ID' 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.mutahar.rocks
// @connect vid.puffyan.us
// @connect yt.didw.to
// @connect y.com.cm
// @connect ytb.trom.tf
// @connect invidious.osi.kr
// @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/Invidious-Instances.md
// Remember to add a @connect for the new url.
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://invidious.mutahar.rocks/api/v1/videos/',
'https://vid.puffyan.us/api/v1/videos/',
'https://sponsor.ajay.app/api/youtubeApiProxy?key=8NpFUCMr2Gq4cy4UrUJPBfGBbRQudhJ8zzex8Gq44RYDywLt3UtbbfDap3KPDbcS&videoID=',
'https://yt.didw.to/api/v1/videos/',
'https://y.com.cm/api/v1/videos/',
'https://sponsor.ajay.app/api/youtubeApiProxy?key=8NpFUCMr2Gq4cy4UrUJPBfGBbRQudhJ8zzex8Gq44RYDywLt3UtbbfDap3KPDbcS&videoID=',
'https://ytb.trom.tf/api/v1/videos/',
'https://invidious.osi.kr/api/v1/videos/',
'https://sponsor.ajay.app/api/youtubeApiProxy?key=8NpFUCMr2Gq4cy4UrUJPBfGBbRQudhJ8zzex8Gq44RYDywLt3UtbbfDap3KPDbcS&videoID=',
'https://inv.riverside.rocks/api/v1/videos/',
'https://invidio.xamh.de/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 headers = [...table.querySelectorAll('thead th')].map(item => item.textContent.trim());
if (headers.includes('Video ID')) {
const columnIndex = headers.indexOf('Video ID');
[...table.querySelectorAll('thead th')][columnIndex].children[0].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]}${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}:\n${responseObject?.response?.error}\nUsing another instance...`);
// Try another random instance that works
const trueUrlsIndexes = [];
UrlWorks.forEach((bool, i) => {
if (bool) {
trueUrlsIndexes.push(i);
}
});
const randomIndex = Math.floor(Math.random() * trueUrlsIndexes.length);
// trueUrlsIndexes length is 0 if all are false
if (trueUrlsIndexes.length !== 0) {
callApis(videoID, videoIdElArray, trueUrlsIndexes[randomIndex]);
} else {
videoIdElArray.forEach(videoIdEl => {
videoIdEl.firstElementChild?.classList.remove('loading');
});
}
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;
});
}
videoIdElArray.forEach(videoIdEl => {
videoIdEl.firstElementChild?.classList.remove('loading');
});
},
onerror: () => {
changeUrlWorksValue(index, false);
console.log(`${requestUrl} doesn't exist anymore.\nUsing another instance...`);
if (!UrlWorks.every(v => v === false)) {
if (index !== URLS_LENGTH - 1) {
callApis(videoID, videoIdElArray, index + 1);
} else {
callApis(videoID, videoIdElArray, 0);
}
} else {
videoIdElArray.forEach(videoIdEl => {
videoIdEl.firstElementChild?.classList.remove('loading');
});
}
},
ontimeout: () => {
console.log(`${requestUrl} timed out.\nUsing another instance...`);
if (!UrlWorks.every(v => v === false)) {
if (index !== URLS_LENGTH - 1) {
callApis(videoID, videoIdElArray, index + 1);
} else {
callApis(videoID, videoIdElArray, 0);
}
} else {
videoIdElArray.forEach(videoIdEl => {
videoIdEl.firstElementChild?.classList.remove('loading');
});
}
}
});
} 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;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment