193 lines
6.2 KiB
JavaScript
193 lines
6.2 KiB
JavaScript
// ==UserScript==
|
|
// @name sb.js userscript
|
|
// @description SponsorBlock userscript
|
|
// @namespace mchang.name
|
|
// @homepage https://github.com/mchangrh/sb.js
|
|
// @icon https://mchangrh.github.io/sb.js/icon.png
|
|
// @version 1.3.2
|
|
// @license LGPL-3.0-or-later
|
|
// @match https://www.youtube.com/watch*
|
|
// @connect sponsor.ajay.app
|
|
// @grant none
|
|
// ==/UserScript==
|
|
/* START OF SETTINGS */
|
|
|
|
// https://wiki.sponsor.ajay.app/w/Types
|
|
const categories = [
|
|
"sponsor",
|
|
"selfpromo",
|
|
"interaction",
|
|
"intro",
|
|
"outro",
|
|
"preview",
|
|
"music_offtopic",
|
|
"exclusive_access",
|
|
"poi_highlight",
|
|
]
|
|
const actionTypes = ["skip", "mute", "full", "poi"]
|
|
const skipThreshold = [0.2, 1] // skip from between time-[0] and time+[1]
|
|
const serverEndpoint = "https://sponsor.ajay.app"
|
|
const skipTracking = true
|
|
const highlightKey = "Enter"
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
|
|
|
|
/* END OF SETTINGS */
|
|
/* sb.js - SponsorBlock for restrictive environments - by mchangrh
|
|
|
|
https://github.com/mchangrh/sb.js
|
|
|
|
Uses SponsorBlock data licensed used under CC BY-NC-SA 4.0 from https://sponsor.ajay.app/
|
|
|
|
LICENCED UNDER LGPL-3.0-or-later */
|
|
const VERSION = "1.3.2" // version constant
|
|
|
|
// initial setup
|
|
let video, videoID, muteEndTime
|
|
let skipSegments = new Map()
|
|
let muteSegments = new Map()
|
|
|
|
// functions
|
|
const getVideoID = () => new URL(window.location.href).searchParams.get("v")
|
|
|
|
function getJSON(url, callback) {
|
|
const xhr = new XMLHttpRequest()
|
|
xhr.open("GET", url)
|
|
xhr.responseType = "json"
|
|
xhr.onload = () => xhr.status == 200 ? callback(null, xhr.response) : callback(xhr.status)
|
|
xhr.send()
|
|
}
|
|
|
|
const trackSkip = uuid => {
|
|
if (!skipTracking) return
|
|
const xhr = new XMLHttpRequest()
|
|
xhr.open("POST", `${serverEndpoint}/api/viewedVideoSponsorTime?UUID=${uuid}`)
|
|
xhr.send()
|
|
}
|
|
|
|
function fetch(videoID) {
|
|
const url = `${serverEndpoint}/api/skipSegments?videoID=${videoID}&categories=${JSON.stringify(categories)}&actionTypes=${JSON.stringify(actionTypes)}`
|
|
const convertSegment = s => [s.segment[0], { end: s.segment[1], uuid: s.UUID }]
|
|
getJSON(url, (err, data) => {
|
|
if (err) return console.error("[SB.js]", "error fetching segments", err)
|
|
data.forEach(s => {
|
|
if (s.actionType === "skip") skipSegments.set(...convertSegment(s))
|
|
else if (s.actionType === "mute") muteSegments.set(...convertSegment(s))
|
|
else if (s.actionType === "full") createVideoLabel(s)
|
|
else if (s.actionType === "poi") createPOILabel(s)
|
|
})
|
|
console.log("[SB.js] Loaded Segments")
|
|
})
|
|
}
|
|
|
|
function skipOrMute() {
|
|
const currentTime = video.currentTime
|
|
// if mute time is over, unmute video
|
|
if (video.muted && currentTime >= muteEndTime) {
|
|
video.muted = false
|
|
muteEndTime = 0
|
|
}
|
|
// check for any skip starts
|
|
const skipEnd = findEndTime(currentTime, skipSegments)
|
|
if (skipEnd) video.currentTime = skipEnd
|
|
// check for any mute starts
|
|
const muteEnd = findEndTime(currentTime, muteSegments)
|
|
if (muteEnd) {
|
|
video.muted = true
|
|
muteEndTime = muteEnd
|
|
}
|
|
}
|
|
|
|
function findEndTime(now, map) {
|
|
let endTime
|
|
for (const startTime of map.keys()) {
|
|
if (
|
|
now + skipThreshold[0] >= startTime &&
|
|
now - startTime <= skipThreshold[1]
|
|
) { // within threshold
|
|
const segment = map.get(startTime)
|
|
endTime = segment.end
|
|
trackSkip(segment.uuid)
|
|
map.delete(startTime) // only use segment once
|
|
for (const overlapStart of map.keys()) {
|
|
// check for overlap
|
|
if (endTime >= overlapStart && overlapStart >= now) {
|
|
// move to end of overlaps
|
|
const overSegment = map.get(overlapStart)
|
|
endTime = overSegment.end
|
|
trackSkip(overSegment.uuid)
|
|
map.delete(overlapStart)
|
|
}
|
|
}
|
|
return endTime // early return
|
|
}
|
|
}
|
|
return endTime
|
|
}
|
|
function createPOILabel(poiLabel) {
|
|
createVideoLabel(poiLabel, "poi")
|
|
// add binding
|
|
const poi_listener = e => {
|
|
if (e.key === highlightKey) {
|
|
video.currentTime = poiLabel.segment[1]
|
|
trackSkip(poiLabel.UUID)
|
|
// remove label
|
|
document.querySelector("#sbjs-label-poi").style.display = "none"
|
|
document.removeEventListener("keydown", poi_listener)
|
|
}
|
|
}
|
|
document.addEventListener("keydown", poi_listener)
|
|
}
|
|
function createVideoLabel(videoLabel, type = "full") {
|
|
// await title
|
|
const title = document.querySelector("#title h1, h1.title.ytd-video-primary-info-renderer")
|
|
if (!title) {
|
|
setTimeout(createVideoLabel, 200, videoLabel)
|
|
return
|
|
}
|
|
const category = videoLabel.category
|
|
const fvString = category => `The entire video is ${category} and is too tightly integrated to be able to seperate`
|
|
const styles = {
|
|
// fg, bg, hover text
|
|
sponsor: ["#0d0", "#111", fvString("sponsor")],
|
|
selfpromo: ["#ff0", "#111", fvString("selfpromo")],
|
|
exclusive_access: ["#085", "#fff", "This video showcases a product, service or location that they've received free or subsidized access to"],
|
|
poi_highlight: ["#f18", "#fff", `Press ${highlightKey} to skip to the highlight`],
|
|
}
|
|
const style = styles[category]
|
|
const label = document.createElement("span")
|
|
label.title = style[2]
|
|
label.innerText = category
|
|
label.id = `sbjs-label-${type}`
|
|
label.style = `color: ${style[1]}; background-color: ${style[0]}; display: flex; margin: 0 5px;`
|
|
// prepend to title
|
|
title.style = "display: flex;"
|
|
title.prepend(label)
|
|
}
|
|
|
|
const reset = () => {
|
|
video = undefined
|
|
videoID = undefined
|
|
muteEndTime = 0
|
|
skipSegments = new Map()
|
|
muteSegments = new Map()
|
|
}
|
|
|
|
function setup() {
|
|
if (videoID === getVideoID()) return // already running correctly
|
|
console.log(`@mchangrh/SB.js ${VERSION} Loaded`)
|
|
console.log(`Uses SponsorBlock data licensed used under CC BY-NC-SA 4.0 from https://sponsor.ajay.app/`)
|
|
if (document.querySelector("#previewbar")) // exit if previewbar exists
|
|
return console.log("[SB.js] Extension Present, Exiting")
|
|
video = document.querySelector("video")
|
|
videoID = getVideoID()
|
|
fetch(videoID)
|
|
if (!video) return console.log("[SB.js] no video")
|
|
video.addEventListener("timeupdate", skipOrMute) // add event listeners
|
|
}
|
|
|
|
// reset on page change
|
|
document.addEventListener("yt-navigate-start", reset)
|
|
// will start setup once event listener fired
|
|
document.addEventListener("yt-navigate-finish", setup)
|
|
setup()
|