Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into chapters

This commit is contained in:
Ajay
2022-02-20 18:37:18 -05:00
82 changed files with 12735 additions and 29320 deletions

View File

@@ -1,7 +1,7 @@
import Config from "./config";
import { SponsorTime, CategorySkipOption, VideoID, SponsorHideType, VideoInfo, StorageChangesObject, ChannelIDInfo, ChannelIDStatus, SponsorSourceType, SegmentUUID, Category, SkipToTimeParams, ToggleSkippable, ActionType, ScheduledTime } from "./types";
import { SponsorTime, CategorySkipOption, VideoID, SponsorHideType, VideoInfo, StorageChangesObject, ChannelIDInfo, ChannelIDStatus, SponsorSourceType, SegmentUUID, Category, SkipToTimeParams, ToggleSkippable, ActionType, ScheduledTime, HashedValue } from "./types";
import { ContentContainer } from "./types";
import { ContentContainer, Keybind } from "./types";
import Utils from "./utils";
const utils = new Utils();
@@ -16,6 +16,7 @@ import * as Chat from "./js-components/chat";
import { SkipButtonControlBar } from "./js-components/skipButtonControlBar";
import { getStartTimeFromUrl } from "./utils/urlParser";
import { findValidElement, getControls, getHashParams, isVisible } from "./utils/pageUtils";
import { isSafari, keybindEquals } from "./utils/configUtils";
import { CategoryPill } from "./render/CategoryPill";
import { AnimationUtils } from "./utils/animationUtils";
import { GenericUtils } from "./utils/genericUtils";
@@ -44,6 +45,7 @@ let lockedCategories: Category[] = [];
// Skips are rescheduled every seeking event.
// Skips are canceled every seeking event
let currentSkipSchedule: NodeJS.Timeout = null;
let currentSkipInterval: NodeJS.Timeout = null;
/** Has the sponsor been skipped */
let sponsorSkipped: boolean[] = [];
@@ -97,6 +99,7 @@ addHotkeyListener();
/** Segments created by the user which have not yet been submitted. */
let sponsorTimesSubmitting: SponsorTime[] = [];
let loadedPreloadedSegment = false;
//becomes true when isInfoFound is called
//this is used to close the popup on YouTube when the other popup opens
@@ -135,6 +138,9 @@ const manualSkipPercentCount = 0.5;
//get messages from the background script and the popup
chrome.runtime.onMessage.addListener(messageListener);
//store pressed modifier keys
const pressedKeys = new Set();
function messageListener(request: Message, sender: unknown, sendResponse: (response: MessageResponse) => void): void | boolean {
//messages from popup script
@@ -206,6 +212,14 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
case "reskip":
reskipSponsorTime(sponsorTimes.find((segment) => segment.UUID === request.UUID));
break;
case "submitVote":
vote(request.type, request.UUID).then((response) => sendResponse(response));
return true;
case "hideSegment":
utils.getSponsorTimeFromUUID(sponsorTimes, request.UUID).hidden = request.type;
utils.addHiddenSegment(sponsorVideoID, request.UUID, request.type);
updatePreviewBar();
break;
}
}
@@ -224,8 +238,8 @@ function contentConfigUpdateListener(changes: StorageChangesObject) {
}
}
if (!Config.configListeners.includes(contentConfigUpdateListener)) {
Config.configListeners.push(contentConfigUpdateListener);
if (!Config.configSyncListeners.includes(contentConfigUpdateListener)) {
Config.configSyncListeners.push(contentConfigUpdateListener);
}
function resetValues() {
@@ -418,9 +432,13 @@ function videoOnReadyListener(): void {
function cancelSponsorSchedule(): void {
if (currentSkipSchedule !== null) {
clearTimeout(currentSkipSchedule);
currentSkipSchedule = null;
}
if (currentSkipInterval !== null) {
clearInterval(currentSkipInterval);
currentSkipInterval = null;
}
}
/**
@@ -482,15 +500,16 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
}
}
const skippingFunction = () => {
const skippingFunction = (forceVideoTime?: number) => {
let forcedSkipTime: number = null;
let forcedIncludeIntersectingSegments = false;
let forcedIncludeNonIntersectingSegments = true;
if (incorrectVideoCheck(videoID, currentSkip)) return;
forceVideoTime ||= video.currentTime;
if ((shouldSkip(currentSkip) || sponsorTimesSubmitting?.some((segment) => segment.segment === currentSkip.segment))
&& video.currentTime >= skipTime[0] && video.currentTime < skipTime[1]) {
&& forceVideoTime >= skipTime[0] && forceVideoTime < skipTime[1]) {
skipToTime({
v: video,
skipTime,
@@ -514,7 +533,22 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
if (timeUntilSponsor <= 0) {
skippingFunction();
} else {
currentSkipSchedule = setTimeout(skippingFunction, timeUntilSponsor * 1000 * (1 / video.playbackRate));
const delayTime = timeUntilSponsor * 1000 * (1 / video.playbackRate);
if (delayTime < 300 && utils.isFirefox() && !isSafari()) {
// For Firefox, use interval instead of timeout near the end to combat imprecise video time
const startIntervalTime = performance.now();
const startVideoTime = video.currentTime;
currentSkipInterval = setInterval(() => {
const intervalDuration = performance.now() - startIntervalTime;
if (intervalDuration >= delayTime || video.currentTime >= skipTime[0]) {
clearInterval(currentSkipInterval);
skippingFunction(Math.max(video.currentTime, startVideoTime + intervalDuration / 1000));
}
}, 5);
} else {
// Schedule for right before to be more precise than normal timeout
currentSkipSchedule = setTimeout(skippingFunction, Math.max(0, delayTime - 30));
}
}
}
@@ -686,17 +720,14 @@ async function sponsorsLookup(id: string, keepOldSubmissions = true) {
setupVideoMutationListener();
// Create categories list
const categories: string[] = [];
for (const categorySelection of Config.config.categorySelections) {
categories.push(categorySelection.name);
}
const categories: string[] = Config.config.categorySelections.map((category) => category.name);
const extraRequestData: Record<string, unknown> = {};
const hashParams = getHashParams();
if (hashParams.requiredSegment) extraRequestData.requiredSegment = hashParams.requiredSegment;
// Check for hashPrefix setting
const hashPrefix = (await utils.getHash(id, 1)).substr(0, 4);
const hashPrefix = (await utils.getHash(id, 1)).slice(0, 4) as VideoID & HashedValue;
const response = await utils.asyncRequestToServer('GET', "/api/skipSegments/" + hashPrefix, {
categories,
actionTypes: getEnabledActionTypes(),
@@ -735,10 +766,10 @@ async function sponsorsLookup(id: string, keepOldSubmissions = true) {
// Hide all submissions smaller than the minimum duration
if (Config.config.minDuration !== 0) {
for (let i = 0; i < sponsorTimes.length; i++) {
if (sponsorTimes[i].segment[1] - sponsorTimes[i].segment[0] < Config.config.minDuration
&& sponsorTimes[i].actionType !== ActionType.Poi) {
sponsorTimes[i].hidden = SponsorHideType.MinimumDuration;
for (const segment of sponsorTimes) {
const duration = segment[1] - segment[0];
if (duration > 0 && duration < Config.config.minDuration) {
segment.hidden = SponsorHideType.MinimumDuration;
}
}
}
@@ -754,6 +785,18 @@ async function sponsorsLookup(id: string, keepOldSubmissions = true) {
}
}
// See if some segments should be hidden
const downvotedData = Config.local.downvotedSegments[hashPrefix];
if (downvotedData) {
for (const segment of sponsorTimes) {
const hashedUUID = await utils.getHash(segment.UUID, 1);
const segmentDownvoteData = downvotedData.segments.find((downvote) => downvote.uuid === hashedUUID);
if (segmentDownvoteData) {
segment.hidden = segmentDownvoteData.hidden;
}
}
}
startSkipScheduleCheckingForStartSponsors();
//update the preview bar
@@ -816,7 +859,7 @@ async function updateVipInfo(): Promise<boolean> {
}
async function lockedCategoriesLookup(id: string): Promise<void> {
const hashPrefix = (await utils.getHash(id, 1)).substr(0, 4);
const hashPrefix = (await utils.getHash(id, 1)).slice(0, 4);
const response = await utils.asyncRequestToServer("GET", "/api/lockCategories/" + hashPrefix);
if (response.ok) {
@@ -926,6 +969,8 @@ async function getVideoInfo(): Promise<void> {
function getYouTubeVideoID(document: Document): string | boolean {
const url = document.URL;
// clips should never skip, going from clip to full video has no indications.
if (url.includes("youtube.com/clip/")) return false;
// skip to URL if matches youtube watch or invidious or matches youtube pattern
if ((!url.includes("youtube.com")) || url.includes("/watch") || url.includes("/shorts/") || url.includes("playlist")) return getYouTubeVideoIDFromURL(url);
// skip to document and don't hide if on /embed/
@@ -979,8 +1024,8 @@ function getYouTubeVideoIDFromURL(url: string): string | boolean {
return id.length == 11 ? id : false;
} else if (urlObject.pathname.startsWith("/embed/") || urlObject.pathname.startsWith("/shorts/")) {
try {
const id = urlObject.pathname.split("/")[2];
if (id && id.length >= 11) return id.substr(0, 11);
const id = urlObject.pathname.split("/")[2]
if (id?.length >=11 ) return id.slice(0, 11);
} catch (e) {
console.error("[SB] Video ID not valid for " + url);
return false;
@@ -1054,7 +1099,8 @@ async function whitelistCheck() {
const getChannelID = () => videoInfo?.videoDetails?.channelId
?? document.querySelector(".ytd-channel-name a")?.getAttribute("href")?.replace(/\/.+\//, "") // YouTube
?? document.querySelector(".ytp-title-channel-logo")?.getAttribute("href")?.replace(/https:\/.+\//, "") // YouTube Embed
?? document.querySelector("a > .channel-profile")?.parentElement?.getAttribute("href")?.replace(/\/.+\//, ""); // Invidious
?? document.querySelector("a > .channel-profile")?.parentElement?.getAttribute("href")?.replace(/\/.+\//, "") // Invidious
?? document.querySelector("a.slim-owner-icon-and-title")?.getAttribute("href")?.replace(/\/.+\//, ""); // Mobile YouTube
try {
await utils.wait(() => !!getChannelID(), 6000, 20);
@@ -1285,14 +1331,21 @@ function skipToTime({v, skipTime, skippingSegments, openNotice, forceAutoSkip, u
if (autoSkip && Config.config.audioNotificationOnSkip) {
const beep = new Audio(chrome.runtime.getURL("icons/beep.ogg"));
beep.volume = video.volume * 0.1;
const oldMetadata = navigator.mediaSession.metadata
beep.play();
beep.addEventListener("ended", () => {
navigator.mediaSession.metadata = null;
setTimeout(() =>
navigator.mediaSession.metadata = oldMetadata
);
})
}
if (!autoSkip
&& skippingSegments.length === 1
&& skippingSegments[0].actionType === ActionType.Poi) {
skipButtonControlBar.enable(skippingSegments[0]);
if (onMobileYouTube) skipButtonControlBar.setShowKeybindHint(false);
if (onMobileYouTube || Config.config.skipKeybind == null) skipButtonControlBar.setShowKeybindHint(false);
activeSkipKeybindElement?.setShowKeybindHint(false);
activeSkipKeybindElement = skipButtonControlBar;
@@ -1301,7 +1354,7 @@ function skipToTime({v, skipTime, skippingSegments, openNotice, forceAutoSkip, u
//send out the message saying that a sponsor message was skipped
if (!Config.config.dontShowNotice || !autoSkip) {
const newSkipNotice = new SkipNotice(skippingSegments, autoSkip, skipNoticeContentContainer, unskipTime);
if (onMobileYouTube) newSkipNotice.setShowKeybindHint(false);
if (onMobileYouTube || Config.config.skipKeybind == null) newSkipNotice.setShowKeybindHint(false);
skipNotices.push(newSkipNotice);
activeSkipKeybindElement?.setShowKeybindHint(false);
@@ -1508,7 +1561,8 @@ function startOrEndTimingNewSegment() {
}
// Save the newly created segment
Config.config.segmentTimes.set(sponsorVideoID, sponsorTimesSubmitting);
Config.config.unsubmittedSegments[sponsorVideoID] = sponsorTimesSubmitting;
Config.forceSyncUpdate("unsubmittedSegments");
// Make sure they know if someone has already submitted something it while they were watching
sponsorsLookup(sponsorVideoID);
@@ -1530,7 +1584,8 @@ function isSegmentCreationInProgress(): boolean {
function cancelCreatingSegment() {
if (isSegmentCreationInProgress()) {
sponsorTimesSubmitting.splice(sponsorTimesSubmitting.length - 1, 1);
Config.config.segmentTimes.set(sponsorVideoID, sponsorTimesSubmitting);
Config.config.unsubmittedSegments[sponsorVideoID] = sponsorTimesSubmitting;
Config.forceSyncUpdate("unsubmittedSegments");
if (sponsorTimesSubmitting.length <= 0) resetSponsorSubmissionNotice();
}
@@ -1540,7 +1595,7 @@ function cancelCreatingSegment() {
}
function updateSponsorTimesSubmitting(getFromConfig = true) {
const segmentTimes = Config.config.segmentTimes.get(sponsorVideoID);
const segmentTimes = Config.config.unsubmittedSegments[sponsorVideoID];
//see if this data should be saved in the sponsorTimesSubmitting variable
if (getFromConfig && segmentTimes != undefined) {
@@ -1629,11 +1684,15 @@ function openInfoMenu() {
const copy = <HTMLImageElement> popup.querySelector("#sbPopupIconCopyUserID");
const check = <HTMLImageElement> popup.querySelector("#sbPopupIconCheck");
const refreshSegments = <HTMLImageElement> popup.querySelector("#refreshSegments");
const heart = <HTMLImageElement> popup.querySelector(".sbHeart");
const close = <HTMLImageElement> popup.querySelector("#sbCloseDonate");
logo.src = chrome.extension.getURL("icons/IconSponsorBlocker256px.png");
settings.src = chrome.extension.getURL("icons/settings.svg");
edit.src = chrome.extension.getURL("icons/pencil.svg");
copy.src = chrome.extension.getURL("icons/clipboard.svg");
check.src = chrome.extension.getURL("icons/check.svg");
heart.src = chrome.extension.getURL("icons/heart.svg");
close.src = chrome.extension.getURL("icons/close.png");
refreshSegments.src = chrome.extension.getURL("icons/refresh.svg");
parentNode.insertBefore(popup, parentNode.firstChild);
@@ -1670,7 +1729,7 @@ function closeInfoMenuAnd<T>(func: () => T): T {
function clearSponsorTimes() {
const currentVideoID = sponsorVideoID;
const sponsorTimes = Config.config.segmentTimes.get(currentVideoID);
const sponsorTimes = Config.config.unsubmittedSegments[currentVideoID];
if (sponsorTimes != undefined && sponsorTimes.length > 0) {
const confirmMessage = chrome.i18n.getMessage("clearThis") + getSegmentsMessage(sponsorTimes)
@@ -1680,7 +1739,8 @@ function clearSponsorTimes() {
resetSponsorSubmissionNotice();
//clear the sponsor times
Config.config.segmentTimes.delete(currentVideoID);
delete Config.config.unsubmittedSegments[currentVideoID];
Config.forceSyncUpdate("unsubmittedSegments");
//clear sponsor times submitting
sponsorTimesSubmitting = [];
@@ -1691,7 +1751,7 @@ function clearSponsorTimes() {
}
//if skipNotice is null, it will not affect the UI
async function vote(type: number, UUID: SegmentUUID, category?: Category, skipNotice?: SkipNoticeComponent): Promise<void> {
async function vote(type: number, UUID: SegmentUUID, category?: Category, skipNotice?: SkipNoticeComponent): Promise<VoteResponse> {
if (skipNotice !== null && skipNotice !== undefined) {
//add loading info
skipNotice.addVoteButtonInfo.bind(skipNotice)(chrome.i18n.getMessage("Loading"))
@@ -1719,6 +1779,8 @@ async function vote(type: number, UUID: SegmentUUID, category?: Category, skipNo
}
}
}
return response;
}
async function voteAsync(type: number, UUID: SegmentUUID, category?: Category): Promise<VoteResponse> {
@@ -1748,7 +1810,29 @@ async function voteAsync(type: number, UUID: SegmentUUID, category?: Category):
type: type,
UUID: UUID,
category: category
}, resolve);
}, (response) => {
if (response.successType === 1) {
// Change the sponsor locally
const segment = utils.getSponsorTimeFromUUID(sponsorTimes, UUID);
if (segment) {
if (type === 0) {
segment.hidden = SponsorHideType.Downvoted;
} else if (category) {
segment.category = category;
} else if (type === 1) {
segment.hidden = SponsorHideType.Visible;
}
if (!category && !Config.config.isVip) {
utils.addHiddenSegment(sponsorVideoID, segment.UUID, segment.hidden);
}
updatePreviewBar();
}
}
resolve(response);
});
});
}
@@ -1807,7 +1891,8 @@ async function sendSubmitMessage() {
}
//update sponsorTimes
Config.config.segmentTimes.set(sponsorVideoID, sponsorTimesSubmitting);
Config.config.unsubmittedSegments[sponsorVideoID] = sponsorTimesSubmitting;
Config.forceSyncUpdate("unsubmittedSegments");
// Check to see if any of the submissions are below the minimum duration set
if (Config.config.minDuration > 0) {
@@ -1834,7 +1919,8 @@ async function sendSubmitMessage() {
stopAnimation();
// Remove segments from storage since they've already been submitted
Config.config.segmentTimes.delete(sponsorVideoID);
delete Config.config.unsubmittedSegments[sponsorVideoID];
Config.forceSyncUpdate("unsubmittedSegments");
const newSegments = sponsorTimesSubmitting;
try {
@@ -1883,7 +1969,7 @@ function getSegmentsMessage(sponsorTimes: SponsorTime[]): string {
let timeMessage = utils.getFormattedTime(sponsorTimes[i].segment[s]);
//if this is an end time
if (s == 1) {
timeMessage = " to " + timeMessage;
timeMessage = " " + chrome.i18n.getMessage("to") + " " + timeMessage;
} else if (i > 0) {
//add commas if necessary
timeMessage = ", " + timeMessage;
@@ -1908,30 +1994,47 @@ function addPageListeners(): void {
function addHotkeyListener(): void {
document.addEventListener("keydown", hotkeyListener);
document.addEventListener("keyup", (e) => pressedKeys.delete(e.key));
document.addEventListener("focus", (e) => pressedKeys.clear());
}
function hotkeyListener(e: KeyboardEvent): void {
if (["textarea", "input"].includes(document.activeElement?.tagName?.toLowerCase())
|| document.activeElement?.id?.toLowerCase()?.includes("editable")) return;
const key = e.key;
if (["Alt", "Control", "Shift", "AltGraph"].includes(e.key)) {
pressedKeys.add(e.key);
return;
}
const key:Keybind = {key: e.key, code: e.code, alt: pressedKeys.has("Alt"), ctrl: pressedKeys.has("Control"), shift: pressedKeys.has("Shift")};
const skipKey = Config.config.skipKeybind;
const startSponsorKey = Config.config.startSponsorKeybind;
const submitKey = Config.config.submitKeybind;
switch (key) {
case skipKey:
if (activeSkipKeybindElement) {
if (!pressedKeys.has("AltGraph")) {
if (keybindEquals(key, skipKey)) {
if (activeSkipKeybindElement)
activeSkipKeybindElement.toggleSkip.call(activeSkipKeybindElement);
}
break;
case startSponsorKey:
return;
} else if (keybindEquals(key, startSponsorKey)) {
startOrEndTimingNewSegment();
break;
case submitKey:
return;
} else if (keybindEquals(key, submitKey)) {
submitSponsorTimes();
break;
return;
}
}
//legacy - to preserve keybinds for skipKey, startSponsorKey and submitKey for people who set it before the update. (shouldn't be changed for future keybind options)
if (key.key == skipKey?.key && skipKey.code == null && !keybindEquals(Config.syncDefaults.skipKeybind, skipKey)) {
if (activeSkipKeybindElement)
activeSkipKeybindElement.toggleSkip.call(activeSkipKeybindElement);
} else if (key.key == startSponsorKey?.key && startSponsorKey.code == null && !keybindEquals(Config.syncDefaults.startSponsorKeybind, startSponsorKey)) {
startOrEndTimingNewSegment();
} else if (key.key == submitKey?.key && submitKey.code == null && !keybindEquals(Config.syncDefaults.submitKeybind, submitKey)) {
submitSponsorTimes();
}
}
@@ -1981,7 +2084,6 @@ function sendRequestToCustomServer(type, fullAddress, callback) {
function updateAdFlag(): void {
const wasAdPlaying = isAdPlaying;
isAdPlaying = document.getElementsByClassName('ad-showing').length > 0;
if(wasAdPlaying != isAdPlaying) {
updatePreviewBar();
updateVisibilityOfPlayerControlsButton();
@@ -2017,8 +2119,12 @@ function showTimeWithoutSkips(skippedDuration: number): void {
}
function checkForPreloadedSegment() {
if (loadedPreloadedSegment) return;
loadedPreloadedSegment = true;
const hashParams = getHashParams();
let pushed = false;
const segments = hashParams.segments;
if (Array.isArray(segments)) {
for (const segment of segments) {
@@ -2031,8 +2137,15 @@ function checkForPreloadedSegment() {
actionType: segment.actionType ? segment.actionType : ActionType.Skip,
source: SponsorSourceType.Local
});
pushed = true;
}
}
}
}
if (pushed) {
Config.config.unsubmittedSegments[sponsorVideoID] = sponsorTimesSubmitting;
Config.forceSyncUpdate("unsubmittedSegments");
}
}