diff --git a/config/defaults.js b/config/defaults.js index 12f6a2486..f05332415 100644 --- a/config/defaults.js +++ b/config/defaults.js @@ -63,7 +63,19 @@ const defaultConfig = { volumeDown: "Shift+PageDown" }, savedVolume: undefined //plugin save volume between session here - } + }, + sponsorblock: { + enabled: false, + apiURL: "https://sponsor.ajay.app", + categories: [ + "sponsor", + "intro", + "outro", + "interaction", + "selfpromo", + "music_offtopic", + ], + }, }, }; diff --git a/jest.config.js b/jest.config.js index 21e1944c3..472d55840 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,6 +2,5 @@ module.exports = { globals: { __APP__: undefined, // A different app will be launched in each test environment }, - testEnvironment: "./tests/environment", testTimeout: 30000, // 30s }; diff --git a/plugins/disable-autoplay/front.js b/plugins/disable-autoplay/front.js index c40eb7fdf..eb1b72db9 100644 --- a/plugins/disable-autoplay/front.js +++ b/plugins/disable-autoplay/front.js @@ -1,25 +1,10 @@ -let videoElement = null; - -const observer = new MutationObserver((mutations, observer) => { - if (!videoElement) { - videoElement = document.querySelector("video"); - } - - if (videoElement) { - videoElement.ontimeupdate = () => { - if (videoElement.currentTime === 0 && videoElement.duration !== NaN) { - // auto-confirm-when-paused plugin can interfere here if not disabled! - videoElement.pause(); - } - }; - } -}); - -function observeVideoElement() { - observer.observe(document, { - childList: true, - subtree: true, +const { ontimeupdate } = require("../../providers/video-element"); + +module.exports = () => { + ontimeupdate((videoElement) => { + if (videoElement.currentTime === 0 && videoElement.duration !== NaN) { + // auto-confirm-when-paused plugin can interfere here if not disabled! + videoElement.pause(); + } }); -} - -module.exports = observeVideoElement; +}; diff --git a/plugins/sponsorblock/back.js b/plugins/sponsorblock/back.js new file mode 100644 index 000000000..3c7eb5b4e --- /dev/null +++ b/plugins/sponsorblock/back.js @@ -0,0 +1,51 @@ +const fetch = require("node-fetch"); + +const defaultConfig = require("../../config/defaults"); +const registerCallback = require("../../providers/song-info"); +const { sortSegments } = require("./segments"); + +let videoID; + +module.exports = (win, options) => { + const { apiURL, categories } = { + ...defaultConfig.plugins.sponsorblock, + ...options, + }; + + registerCallback(async (info) => { + const newURL = info.url || win.webContents.getURL(); + const newVideoID = new URL(newURL).searchParams.get("v"); + + if (videoID !== newVideoID) { + videoID = newVideoID; + const segments = await fetchSegments(apiURL, categories); + win.webContents.send("sponsorblock-skip", segments); + } + }); +}; + +const fetchSegments = async (apiURL, categories) => { + const sponsorBlockURL = `${apiURL}/api/skipSegments?videoID=${videoID}&categories=${JSON.stringify( + categories + )}`; + try { + const resp = await fetch(sponsorBlockURL, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + redirect: "follow", + }); + if (resp.status !== 200) { + return []; + } + const segments = await resp.json(); + const sortedSegments = sortSegments( + segments.map((submission) => submission.segment) + ); + + return sortedSegments; + } catch { + return []; + } +}; diff --git a/plugins/sponsorblock/front.js b/plugins/sponsorblock/front.js new file mode 100644 index 000000000..4f248bfa9 --- /dev/null +++ b/plugins/sponsorblock/front.js @@ -0,0 +1,27 @@ +const { ipcRenderer } = require("electron"); + +const is = require("electron-is"); + +const { ontimeupdate } = require("../../providers/video-element"); + +let currentSegments = []; + +module.exports = () => { + ipcRenderer.on("sponsorblock-skip", (_, segments) => { + currentSegments = segments; + }); + + ontimeupdate((videoElement) => { + currentSegments.forEach((segment) => { + if ( + videoElement.currentTime >= segment[0] && + videoElement.currentTime <= segment[1] + ) { + videoElement.currentTime = segment[1]; + if (is.dev()) { + console.log("SponsorBlock: skipping segment", segment); + } + } + }); + }); +}; diff --git a/plugins/sponsorblock/segments.js b/plugins/sponsorblock/segments.js new file mode 100644 index 000000000..c12a9e885 --- /dev/null +++ b/plugins/sponsorblock/segments.js @@ -0,0 +1,29 @@ +// Segments are an array [ [start, end], … ] +module.exports.sortSegments = (segments) => { + segments.sort((segment1, segment2) => + segment1[0] === segment2[0] + ? segment1[1] - segment2[1] + : segment1[0] - segment2[0] + ); + + const compiledSegments = []; + let currentSegment; + + segments.forEach((segment) => { + if (!currentSegment) { + currentSegment = segment; + return; + } + + if (currentSegment[1] < segment[0]) { + compiledSegments.push(currentSegment); + currentSegment = segment; + return; + } + + currentSegment[1] = Math.max(currentSegment[1], segment[1]); + }); + compiledSegments.push(currentSegment); + + return compiledSegments; +}; diff --git a/plugins/sponsorblock/tests/segments.test.js b/plugins/sponsorblock/tests/segments.test.js new file mode 100644 index 000000000..dbc3d4b07 --- /dev/null +++ b/plugins/sponsorblock/tests/segments.test.js @@ -0,0 +1,34 @@ +const { sortSegments } = require("../segments"); + +test("Segment sorting", () => { + expect( + sortSegments([ + [0, 3], + [7, 8], + [5, 6], + ]) + ).toEqual([ + [0, 3], + [5, 6], + [7, 8], + ]); + + expect( + sortSegments([ + [0, 5], + [6, 8], + [4, 6], + ]) + ).toEqual([[0, 8]]); + + expect( + sortSegments([ + [0, 6], + [7, 8], + [4, 6], + ]) + ).toEqual([ + [0, 6], + [7, 8], + ]); +}); diff --git a/providers/video-element.js b/providers/video-element.js new file mode 100644 index 000000000..7be61c899 --- /dev/null +++ b/providers/video-element.js @@ -0,0 +1,22 @@ +let videoElement = null; + +module.exports.ontimeupdate = (cb) => { + const observer = new MutationObserver((mutations, observer) => { + if (!videoElement) { + videoElement = document.querySelector("video"); + if (videoElement) { + observer.disconnect(); + videoElement.ontimeupdate = () => cb(videoElement); + } + } + }); + + if (!videoElement) { + observer.observe(document, { + childList: true, + subtree: true, + }); + } else { + videoElement.ontimeupdate = () => cb(videoElement); + } +}; diff --git a/tests/index.test.js b/tests/index.test.js index 9ab4bf634..ccb261cad 100644 --- a/tests/index.test.js +++ b/tests/index.test.js @@ -1,3 +1,7 @@ +/** + * @jest-environment ./tests/environment + */ + describe("YouTube Music App", () => { const app = global.__APP__;