// ==UserScript==
// @name Brazenvoid's Base Resource
// @namespace brazenvoid
// @version 1.4.12
// @author brazenvoid
// @license GPL-3.0-only
// @description Base library for my scripts
// @run-at document-end
// ==/UserScript==
const sleep = (milliseconds) => {
return new Promise(resolve => setTimeout(resolve, milliseconds))
}
function _generateClassesIdentificationRegex (classes) {
return new RegExp('(\\s|^)' + classes + '(\\s|$)')
}
function addClasses (node, classes) {
if (!hasClasses(node,classes)) {
if (node.className !== '') {
node.className += ' '
}
node.className += classes
}
return node
}
function hasClasses (node, classes) {
return !!node.className.match(_generateClassesIdentificationRegex(classes))
}
function removeClasses (node, classes) {
if (hasClasses(node,classes)) {
node.className = node.className.replace(_generateClassesIdentificationRegex(classes), ' ')
}
return node
}
class CaseConverters {
toCamel (text) {
return text.replace(/(?:^\w|[A-Z]|\b\w)/g, function (letter, index) {
return index === 0 ? letter.toLowerCase() : letter.toUpperCase()
}).replace(/\s+/g, '')
}
toKebab (text) {
return text.toLowerCase().replace(' ', '-')
}
toKebabFromSnake (text) {
return text.replace('_', '-')
}
toNormalFromKebab (text) {
return text.replace('-', ' ')
}
toNormalFromSnake (text) {
return text.replace('_', ' ')
}
toSnake (text) {
return text.toLowerCase().replace(' ', '_')
}
}
class ChildObserver {
static observe (nodes, handler, doInitialRun = false) {
let instance = new ChildObserver(handler)
instance.observeNodes(nodes, doInitialRun)
return instance;
}
constructor (handler) {
this.config = {
attributes: false,
childList: true,
subtree: false
}
this.handler = handler
this.observer = new MutationObserver(function (mutations) {
for (let mutation of mutations) {
handler(mutation.target)
}
})
}
observeNodes (nodes, doInitialRun = false) {
nodes = (Array.isArray(nodes) || nodes instanceof NodeList) ? nodes : [nodes];
for (let node of nodes) {
if (doInitialRun) {
this.handler(node)
}
this.observer.observe(node, this.config)
}
}
}
class LocalStore {
constructor (key, defaultStore) {
this.onDefaultsLoaded = null
this.onRetrieval = null
this.onUpdated = null
this._key = key
this._store = {}
this._defaultStore = this._toJSON(defaultStore)
}
_fromJSON (json) {
let arrayObject = {}
let parsedJSON = JSON.parse(json)
let store = {}
for (let property in parsedJSON.arrays) {
arrayObject = JSON.parse(parsedJSON.arrays[property])
store[property] = []
for (let key in arrayObject) {
store[property].push(arrayObject[key])
}
}
for (let property in parsedJSON.objects) {
store[property] = this._fromJSON(parsedJSON.objects[property])
}
for (let property in parsedJSON.properties) {
store[property] = parsedJSON.properties[property]
}
return store
}
_getStore () {
return window.localStorage.getItem(this._key)
}
_getDefaults () {
return this._fromJSON(this._defaultStore)
}
_toJSON (store) {
let arrayToObject = {}
let json = {arrays: {}, objects: {}, properties: {}}
for (let property in store) {
if (typeof store[property] === 'object') {
if (Array.isArray(store[property])) {
for (let key in store[property]) {
arrayToObject[key] = store[property][key]
}
json.arrays[property] = JSON.stringify(arrayToObject)
} else {
json.objects[property] = this._toJSON(store[property])
}
} else {
json.properties[property] = store[property]
}
}
return JSON.stringify(json)
}
delete () {
window.localStorage.removeItem(this._key)
return this
}
get () {
return this._store
}
restoreDefaults () {
this._store = this._getDefaults()
if (this.onDefaultsLoaded !== null) {
this.onDefaultsLoaded(this._store)
}
return this
}
retrieve () {
let storedStore = this._getStore()
if (storedStore === null) {
this.restoreDefaults()
} else {
this._store = this._fromJSON(storedStore)
}
if (this.onRetrieval !== null) {
this.onRetrieval(this._store)
}
return this
}
save () {
window.localStorage.setItem(this._key, this._toJSON(this._store))
if (this.onUpdated !== null) {
this.onUpdated(this._store)
}
return this
}
isPurged () {
return this._getStore() === null
}
}
class Logger {
constructor (enableDebugging) {
this.enableDebugging = enableDebugging
}
_log (message) {
if (this.enableDebugging) {
console.log(message)
}
}
logTaskCompletion (task) {
this._log('Completed: ' + task)
this.logSeparator()
}
logSeparator () {
this._log('------------------------------------------------------------------------------------')
}
logValidation (filterName, validationResult = null) {
this._log('Satisfies ' + filterName + ' Filter: ' + (validationResult ? 'true' : 'false'))
}
logVideoCheck (videoName) {
this._log('Checking Video: ' + videoName)
}
}
class SelectorGenerator {
constructor (selectorPrefix) {
this.prefix = selectorPrefix
}
getSelector (selector) {
return this.prefix + selector
};
getSelectorFromName (name) {
return CaseConverters.prototype.toKebab(name)
}
getSettingsInputSelector (settingName) {
return this.getSelector(this.getSelectorFromName(settingName) + '-setting')
}
getStatLabelSelector (statisticType) {
return this.getSelector(this.getSelectorFromName(statisticType) + '-stat')
};
}
class StatisticsRecorder {
constructor (logger, selectorGenerator) {
this.logger = logger
this.selectorGenerator = selectorGenerator
this.statistics = {Total: 0}
}
record (statisticType, validationResult, value = 1, log = true) {
if (!validationResult) {
if (typeof this.statistics[statisticType] !== 'undefined') {
this.statistics[statisticType] += value
} else {
this.statistics[statisticType] = value
}
this.statistics.Total += value
}
if (log) {
this.logger.logValidation(statisticType, validationResult)
}
}
reset () {
for (const statisticType in this.statistics) {
this.statistics[statisticType] = 0
}
}
updateUI () {
let label, labelSelector
for (const statisticType in this.statistics) {
labelSelector = this.selectorGenerator.getStatLabelSelector(statisticType)
label = document.getElementById(labelSelector)
if (label !== null) {
label.textContent = this.statistics[statisticType]
}
}
}
}
class UIGenerator {
constructor (showUI, selectorGenerator) {
this.buttonBackroundColor = 'unset'
this.selectorGenerator = selectorGenerator
this.showUI = showUI
}
_populdateChildren (node, children) {
for (let child of children) {
node.appendChild(child)
}
return node
}
appendToBody (node) {
document.getElementsByTagName('body')[0].appendChild(node)
}
createFormButton (caption, onClick) {
let button = document.createElement('button')
button.textContent = caption
button.style.backgroundColor = this.buttonBackroundColor
button.style.height = '30px'
button.style.width = '100%'
button.addEventListener('click', onClick)
return button
}
createFormGroup (children) {
let divFormGroup = document.createElement('div')
divFormGroup.style.display = 'block'
divFormGroup.style.height = '18px'
divFormGroup.style.marginBottom = '2px'
divFormGroup.style.padding = '5px 0'
return this._populdateChildren(divFormGroup, children)
}
createFormGroupDropdown (id, keyValuePairs, defaultValue = null) {
let dropdown = document.createElement('select'), item
dropdown.id = id
dropdown.style.float = 'right'
dropdown.style.height = '18px'
dropdown.style.textAlign = 'center'
dropdown.style.width = '100px'
for (let [key, value] of keyValuePairs) {
item = document.createElement('option')
item.textContent = value
item.value = key
dropdown.appendChild(item)
}
dropdown.value = defaultValue === null ? keyValuePairs[0][0] : defaultValue
return dropdown
}
createFormGroupInput (id, type, defaultValue = null) {
let inputFormGroup = document.createElement('input')
inputFormGroup.id = id
inputFormGroup.type = type
inputFormGroup.style.height = '18px'
inputFormGroup.style.textAlign = 'center'
switch (type) {
case 'number':
case 'text':
inputFormGroup.style.float = 'right'
inputFormGroup.style.width = '100px'
if (defaultValue !== null) {
inputFormGroup.value = defaultValue
}
break
case 'radio':
case 'checkbox':
inputFormGroup.style.float = 'left'
inputFormGroup.style.marginRight = '5px'
if (defaultValue !== null) {
inputFormGroup.checked = defaultValue
}
break
}
return inputFormGroup
}
createFormGroupLabel (label, inputID = null, inputType = null) {
let labelFormGroup = document.createElement('label')
labelFormGroup.style.padding = '2px 0'
labelFormGroup.textContent = label
if (inputID !== null) {
labelFormGroup.setAttribute('for', inputID)
switch (inputType) {
case 'number':
case 'text':
labelFormGroup.textContent += ': '
labelFormGroup.style.float = 'left'
break
case 'radio':
case 'checkbox':
labelFormGroup.style.float = 'right'
break
}
}
return labelFormGroup
}
createFormGroupStatLabel (statisticType) {
let labelFormGroup = document.createElement('label')
labelFormGroup.id = this.selectorGenerator.getStatLabelSelector(statisticType)
labelFormGroup.style.float = 'right'
labelFormGroup.style.padding = '2px 0'
labelFormGroup.textContent = '0'
return labelFormGroup
}
createSection (IDSuffix, backgroundColor, top, width, children) {
let section = document.createElement('section')
section.id = this.selectorGenerator.getSelector(IDSuffix)
section.style.color = 'black'
section.style.display = this.showUI ? 'block' : 'none'
section.style.fontSize = '12px'
section.style.fontWeight = 'bold'
section.style.position = 'fixed'
section.style.top = top
section.style.left = '0'
section.style.width = width
section.style.padding = '5px 10px'
section.style.backgroundColor = backgroundColor
section.style.zIndex = '1000'
return this._populdateChildren(section, children)
}
createSeparator () {
let separator = document.createElement('hr')
separator.style.margin = '3px'
return separator
}
createSettingsFormActionButton (caption, onClick) {
let button = this.createFormButton(caption, onClick)
button.style.width = '95px'
return button
}
createSettingsFormActions (localStore, applyOnClick, addTopPadding = false) {
let divFormActions = document.createElement('div')
divFormActions.style.display = 'block'
let applyButton = this.createSettingsFormActionButton('Apply', applyOnClick)
applyButton.style.marginRight = '10px'
if (addTopPadding) {
divFormActions.style.paddingTop = '10px'
}
return this._populdateChildren(divFormActions, [applyButton, this.createSettingsFormActionButton('Reset', function () {
localStore.retrieve()
applyOnClick()
})])
}
createSettingsFormGroup (label, inputType = 'text', defaultValue = null) {
let divFormGroup
let inputID = this.selectorGenerator.getSettingsInputSelector(label)
let labelFormGroup = this.createFormGroupLabel(label, inputType, inputID)
let inputFormGroup = this.createFormGroupInput(inputID, inputType, defaultValue)
switch (inputType) {
case 'number':
case 'text':
divFormGroup = this.createFormGroup([labelFormGroup, inputFormGroup])
break
case 'radio':
case 'checkbox':
divFormGroup = this.createFormGroup([inputFormGroup, labelFormGroup])
break
}
return divFormGroup
}
createSettingsDropDownFormGroup (label, keyValuePairs, defaultValue = null) {
let dropdownID = this.selectorGenerator.getSettingsInputSelector(label)
return this.createFormGroup([
this.createFormGroupLabel(label, 'text', dropdownID),
this.createFormGroupDropdown(dropdownID, keyValuePairs, defaultValue)
])
}
createSettingsHideButton (settingsSectionIDSuffix) {
let settingsSectionID = this.selectorGenerator.getSelector(settingsSectionIDSuffix);
return this.createFormButton('Hide', function () {
document.getElementById(settingsSectionID).style.display = 'none'
})
}
createSettingsShowButton (caption, settingsSection, fixed = true) {
let controlButton = document.createElement('button')
controlButton.textContent = caption
if (fixed) {
controlButton.style.backgroundColor = '#ffa31a'
controlButton.style.border = '0'
controlButton.style.color = 'black'
controlButton.style.fontSize = '14px'
controlButton.style.left = '0'
controlButton.style.padding = '15px 0px'
controlButton.style.position = 'fixed'
controlButton.style.writingMode = 'sideways-lr'
controlButton.style.top = '250px'
controlButton.style.width = '30px'
controlButton.style.zIndex = '999'
} else {
controlButton.style.width = '100%'
controlButton.style.margin = '2px 5px'
controlButton.style.padding = '2px 5px'
controlButton.style.backgroundColor = '#ffa31a'
controlButton.style.border = '0'
}
controlButton.addEventListener('click', function () {
let settingsUI = document.getElementById(settingsSection.id)
settingsUI.style.display = settingsUI.style.display === 'none' ? 'block' : 'none'
})
return controlButton
}
createStatisticsFormGroup (statisticsType, label = null) {
if (label === null) {
label = statisticsType
}
return this.createFormGroup([
this.createFormGroupLabel('Filtered ' + label + ' Videos'),
this.createFormGroupStatLabel(statisticsType),
])
}
createStoreControlPanel (localStore) {
let controlPanelDiv = document.createElement('div')
controlPanelDiv.style.textAlign = 'center'
let controlPanelLabel = document.createElement('label')
controlPanelLabel.textContent = 'Store'
controlPanelLabel.style.display = 'block'
controlPanelLabel.style.height = '20px'
controlPanelLabel.style.width = '100%'
let resetButton = this.createStoreControlPanelButton('Reset', 'Reset store values to user defaults', function () {
localStore.restoreDefaults()
})
resetButton.style.margin = '0 5px'
return this._populdateChildren(controlPanelDiv, [
controlPanelLabel,
this.createStoreControlPanelButton('Update', 'Save UI settings in store', function () {
localStore.save()
}),
resetButton,
this.createStoreControlPanelButton('Purge', 'Purge store', function () {
localStore.delete()
}),
])
}
createStoreControlPanelButton (caption, tooltip, onClick) {
let button = this.createFormButton(caption, onClick)
button.title = tooltip
button.style.padding = '0 2px'
button.style.width = '55px'
return button
}
getSettingsInput (label) {
return document.getElementById(this.selectorGenerator.getSettingsInputSelector(label))
}
getSettingsInputCheckedStatus (label) {
return this.getSettingsInput(label).checked
}
getSettingsInputValue (label) {
return this.getSettingsInput(label).value;
}
setSettingsInputCheckedStatus (label, bool) {
this.getSettingsInput(label).checked = bool
}
setSettingsInputValue (label, value) {
this.getSettingsInput(label).value = value
}
}
class Validator {
static iFramesRemover () {
GM_addStyle(`
iframe {
display: none !important;
}
`)
}
constructor (statisticsRecorder) {
this._blacklist = []
this._filters = []
this._optimizedBlacklist = []
this._optimizedSanitizationRules = {}
this._sanitizationRules = []
this._statisticsRecorder = statisticsRecorder
}
addBlacklistFilter (blacklistedWords) {
this._blacklist = blacklistedWords
return this
}
addConditionalFilter (name, shouldBe, getValueCallback) {
return this
}
addRangeFilter (name, conditionCallback) {
return this
}
addSanitizationFilter (santizationRules, videoTitleSelector, videoPageTitleSelector) {
this._sanitizationRules = santizationRules
return this
}
optimize () {
for (let i = 0; i < this._blacklist.length; i++) {
this._optimizedBlacklist[i] = new RegExp(this._blacklist[i], 'ig')
}
for (const substitute in this._sanitizationRules) {
this._optimizedSanitizationRules[substitute] = []
for (let i = 0; i < this._sanitizationRules[substitute].length; i++) {
this._optimizedSanitizationRules[substitute][i] = new RegExp(this._sanitizationRules[substitute][i], 'ig')
}
}
return this
}
sanitize (text) {
for (const substitute in this._optimizedSanitizationRules) {
for (const subject of this._optimizedSanitizationRules[substitute]) {
text = text.replace(subject, substitute)
}
}
return text
}
sanitizeVideoItem(videoNameNode) {
videoNameNode.textContent = this.sanitize(videoNameNode.textContent)
return this
}
sanitizeVideoPage(videoNameNodeSelector) {
let videoNameNode = document.querySelector(videoNameNodeSelector)
if (videoNameNode !== null) {
let sanitizedVideoName = this.sanitize(videoNameNode.textContent)
videoNameNode.textContent = sanitizedVideoName
document.title = sanitizedVideoName
}
return this
}
validateBlackList (text) {
let validationCheck = true
if (this._optimizedBlacklist.length > 0) {
for (const blacklistedWord of this._optimizedBlacklist) {
validationCheck = text.match(blacklistedWord) === null
if (!validationCheck) {
break
}
}
this._statisticsRecorder.record('Blacklist', validationCheck)
}
return validationCheck
}
validateRange (name, value, bounds) {
let validationCheck = true
if (bounds[0] > 0) {
validationCheck = value >= bounds[0]
}
if (bounds[1] > 0) {
validationCheck = value <= bounds[1]
}
this._statisticsRecorder.record(name, validationCheck)
return validationCheck
}
validateRangeFilter (name, lowerBound, upperBound, getValueCallback) {
if (lowerBound > 0 || upperBound > 0) {
return this.validateRange(name, getValueCallback(), [lowerBound, upperBound])
}
return true
}
}