diff --git a/.local/share/qutebrowser/greasemonkey/sb.user.js b/.local/share/qutebrowser/greasemonkey/sb.user.js new file mode 100644 index 0000000..a8209df --- /dev/null +++ b/.local/share/qutebrowser/greasemonkey/sb.user.js @@ -0,0 +1,192 @@ +// ==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()