Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into pr/MRuy/606

# Conflicts:
#	src/popup.ts
This commit is contained in:
Ajay Ramachandran
2021-05-26 22:19:15 -04:00
78 changed files with 4418 additions and 1437 deletions

View File

@@ -37,6 +37,9 @@ chrome.runtime.onMessage.addListener(function (request, sender, callback) {
case "openHelp":
chrome.tabs.create({url: chrome.runtime.getURL('help/index_en.html')});
return;
case "openPage":
chrome.tabs.create({url: chrome.runtime.getURL(request.url)});
return;
case "sendRequest":
sendRequestToCustomServer(request.type, request.url, request.data).then(async (response) => {
callback({
@@ -52,16 +55,6 @@ chrome.runtime.onMessage.addListener(function (request, sender, callback) {
//this allows the callback to be called later
return true;
case "alertPrevious":
if (Config.config.unsubmittedWarning) {
chrome.notifications.create("stillThere" + Math.random(), {
type: "basic",
title: chrome.i18n.getMessage("wantToSubmit") + " " + request.previousVideoID + "?",
message: chrome.i18n.getMessage("leftTimes"),
iconUrl: "./icons/LogoSponsorBlocker256px.png"
});
}
break;
case "registerContentScript":
registerFirefoxContentScript(request);
return false;
@@ -135,19 +128,22 @@ async function submitVote(type: number, UUID: string, category: string) {
if (response.ok) {
return {
successType: 1
successType: 1,
responseText: await response.text()
};
} else if (response.status == 405) {
//duplicate vote
return {
successType: 0,
statusCode: response.status
statusCode: response.status,
responseText: await response.text()
};
} else {
//error while connect
return {
successType: -1,
statusCode: response.status
statusCode: response.status,
responseText: await response.text()
};
}
}

View File

@@ -8,6 +8,8 @@ export interface NoticeProps {
timed?: boolean,
idSuffix?: string,
videoSpeed?: () => number,
fadeIn?: boolean,
// Callback for when this is closed
@@ -19,7 +21,7 @@ export interface NoticeProps {
export interface NoticeState {
noticeTitle: string,
maxCountdownTime?: () => number,
maxCountdownTime: () => number,
countdownTime: number,
countdownText: string,
@@ -28,6 +30,8 @@ export interface NoticeState {
class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
countdownInterval: NodeJS.Timeout;
intervalVideoSpeed: number;
idSuffix: string;
amountOfPreviousNotices: number;
@@ -71,7 +75,9 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
return (
<table id={"sponsorSkipNotice" + this.idSuffix}
className={"sponsorSkipObject sponsorSkipNotice" + (this.props.fadeIn ? " sponsorSkipNoticeFadeIn" : "")}
className={"sponsorSkipObject sponsorSkipNotice"
+ (this.props.fadeIn ? " sponsorSkipNoticeFadeIn" : "")
+ (this.amountOfPreviousNotices > 0 ? " secondSkipNotice" : "")}
style={noticeStyle}
onMouseEnter={() => this.timerMouseEnter()}
onMouseLeave={() => this.timerMouseLeave()}>
@@ -152,7 +158,11 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
countdown(): void {
if (!this.props.timed) return;
const countdownTime = this.state.countdownTime - 1;
const countdownTime = Math.min(this.state.countdownTime - 1, this.state.maxCountdownTime());
if (this.props.videoSpeed && this.intervalVideoSpeed != this.props.videoSpeed()) {
this.setupInterval();
}
if (countdownTime <= 0) {
//remove this from setInterval
@@ -175,12 +185,19 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
countdownTime
})
}
removeFadeAnimation(): void {
//remove the fade out class if it exists
const notice = document.getElementById("sponsorSkipNotice" + this.idSuffix);
notice.classList.remove("sponsorSkipNoticeFadeOut");
notice.style.animation = "none";
}
pauseCountdown(): void {
if (!this.props.timed) return;
//remove setInterval
clearInterval(this.countdownInterval);
if (this.countdownInterval) clearInterval(this.countdownInterval);
this.countdownInterval = null;
//reset countdown and inform the user
@@ -189,10 +206,7 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
countdownText: this.state.countdownManuallyPaused ? chrome.i18n.getMessage("manualPaused") : chrome.i18n.getMessage("paused")
});
//remove the fade out class if it exists
const notice = document.getElementById("sponsorSkipNotice" + this.idSuffix);
notice.classList.remove("sponsorSkipNoticeFadeOut");
notice.style.animation = "none";
this.removeFadeAnimation();
}
startCountdown(): void {
@@ -206,16 +220,29 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
countdownText: null
});
this.countdownInterval = setInterval(this.countdown.bind(this), 1000);
this.setupInterval();
}
setupInterval(): void {
if (this.countdownInterval) clearInterval(this.countdownInterval);
const intervalDuration = this.props.videoSpeed ? 1000 / this.props.videoSpeed() : 1000;
this.countdownInterval = setInterval(this.countdown.bind(this), intervalDuration);
if (this.props.videoSpeed) this.intervalVideoSpeed = this.props.videoSpeed();
}
resetCountdown(): void {
if (!this.props.timed) return;
this.setupInterval();
this.setState({
countdownTime: this.state.maxCountdownTime(),
countdownText: null
});
this.removeFadeAnimation();
}
/**

View File

@@ -5,7 +5,7 @@ import { ContentContainer, SponsorHideType, SponsorTime } from "../types";
import NoticeComponent from "./NoticeComponent";
import NoticeTextSelectionComponent from "./NoticeTextSectionComponent";
enum SkipNoticeAction {
export enum SkipNoticeAction {
None,
Upvote,
Downvote,
@@ -24,23 +24,23 @@ export interface SkipNoticeProps {
}
export interface SkipNoticeState {
noticeTitle: string;
noticeTitle?: string;
messages: string[];
messageOnClick: (event: React.MouseEvent) => unknown;
messages?: string[];
messageOnClick?: (event: React.MouseEvent) => unknown;
countdownTime: number;
maxCountdownTime: () => number;
countdownText: string;
countdownTime?: number;
maxCountdownTime?: () => number;
countdownText?: string;
unskipText: string;
unskipCallback: (index: number) => void;
unskipText?: string;
unskipCallback?: (index: number) => void;
downvoting: boolean;
choosingCategory: boolean;
thanksForVotingText: string; //null until the voting buttons should be hidden
downvoting?: boolean;
choosingCategory?: boolean;
thanksForVotingText?: string; //null until the voting buttons should be hidden
actionState: SkipNoticeAction;
actionState?: SkipNoticeAction;
}
class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeState> {
@@ -91,13 +91,6 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
}
this.idSuffix += this.amountOfPreviousNotices;
if (this.amountOfPreviousNotices > 0) {
//another notice exists
const previousNotice = document.getElementsByClassName("sponsorSkipNotice")[0];
previousNotice.classList.add("secondSkipNotice")
}
// Setup state
this.state = {
noticeTitle,
@@ -134,7 +127,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
render(): React.ReactElement {
const noticeStyle: React.CSSProperties = {
zIndex: 50 + this.amountOfPreviousNotices
zIndex: 1000 + this.amountOfPreviousNotices
}
if (this.contentContainer().onMobileYouTube) {
noticeStyle.bottom = "4em";
@@ -148,6 +141,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
fadeIn={true}
timed={true}
maxCountdownTime={this.state.maxCountdownTime}
videoSpeed={() => this.contentContainer().v?.playbackRate}
ref={this.noticeRef}
closeListener={() => this.closeListener()}>
@@ -203,7 +197,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
style={{marginLeft: "4px"}}
onClick={() => this.prepAction(SkipNoticeAction.Unskip)}>
{this.state.unskipText}
{this.state.unskipText + " (" + Config.config.skipKeybind + ")"}
</button>
</td>
@@ -463,21 +457,23 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
reskip(index: number): void {
this.contentContainer().reskipSponsorTime(this.segments[index]);
//reset countdown
this.setState({
const newState: SkipNoticeState = {
unskipText: chrome.i18n.getMessage("unskip"),
unskipCallback: this.unskip.bind(this),
maxCountdownTime: () => 4,
countdownTime: 4
});
};
// See if the title should be changed
if (!this.autoSkip) {
this.setState({
noticeTitle: chrome.i18n.getMessage("noticeTitle")
});
}
newState.noticeTitle = chrome.i18n.getMessage("noticeTitle");
}
//reset countdown
this.setState(newState, () => {
this.noticeRef.current.resetCountdown();
});
}
afterVote(segment: SponsorTime, type: number, category: string): void {

View File

@@ -340,13 +340,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
deleteTime(): void {
const sponsorTimes = this.props.contentContainer().sponsorTimesSubmitting;
const index = this.props.index;
const removingIncomplete = sponsorTimes[index].segment.length < 2;
//if it is not a complete sponsor time
if (sponsorTimes[index].segment.length < 2) {
//update video player
this.props.contentContainer().changeStartSponsorButton(true, false);
}
sponsorTimes.splice(index, 1);
//save this
@@ -357,13 +352,16 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
//if they are all removed
if (sponsorTimes.length == 0) {
this.props.submissionNotice.cancel();
//update video player
this.props.contentContainer().changeStartSponsorButton(true, false);
} else {
//update display
this.props.submissionNotice.forceUpdate();
}
//if it is not a complete segment, or all are removed
if (sponsorTimes.length === 0 || removingIncomplete) {
//update video player
this.props.contentContainer().updateEditButtonsOnPlayer();
}
}
configUpdate(): void {

View File

@@ -32,8 +32,6 @@ class SubmissionNoticeComponent extends React.Component<SubmissionNoticeProps, S
videoObserver: MutationObserver;
showingYouCapNotice: boolean;
constructor(props: SubmissionNoticeProps) {
super(props);
this.noticeRef = React.createRef();
@@ -47,7 +45,7 @@ class SubmissionNoticeComponent extends React.Component<SubmissionNoticeProps, S
this.state = {
noticeTitle,
messages: [],
idSuffix: "SubmissionNotice",
idSuffix: "SubmissionNotice"
}
}
@@ -89,8 +87,6 @@ class SubmissionNoticeComponent extends React.Component<SubmissionNoticeProps, S
</td>
</tr>
{this.getYouCapMessage()}
{/* Last Row */}
<tr id={"sponsorSkipNoticeSecondRow" + this.state.idSuffix}>
@@ -117,35 +113,6 @@ class SubmissionNoticeComponent extends React.Component<SubmissionNoticeProps, S
);
}
/** TODO: Remove */
getYouCapMessage(): JSX.Element {
if (Config.config.sponsorTimesContributed < 20
|| (Config.config.hasShownYouCapNotice && !this.showingYouCapNotice)) {
return;
}
Config.config.hasShownYouCapNotice = true;
if (!this.showingYouCapNotice) {
this.showingYouCapNotice = true;
}
return (
<tr style={{textAlign: "center"}}>
<p style={{width: "300px", textAlign: "center", display: "inline-block", fontSize: "11px"}}>
Like contributing to crowdsourced projects?
Consider checking out <a href="https://gist.github.com/ajayyy/6f2cf90dd66e51067a7ab5e63544cd4e" style={{textDecoration: "underline"}}>YouCap or NekoCap</a>,
new open-source replacements for YouTube{"'"}s now defunct community captions.
</p>
<img src={chrome.extension.getURL("icons/close.png")}
style={{padding: "0", margin: "auto"}}
className="sponsorSkipObject sponsorSkipNoticeButton sponsorSkipNoticeCloseButton"
onClick={() => { this.showingYouCapNotice = false; this.forceUpdate(); }}>
</img>
</tr>
);
}
getSponsorTimeMessages(): JSX.Element[] | JSX.Element {
const elements: JSX.Element[] = [];
this.timeEditRefs = [];

View File

@@ -1,15 +1,17 @@
import * as CompileConfig from "../config.json";
import { CategorySelection, CategorySkipOption, PreviewBarOption, SponsorTime, StorageChangesObject } from "./types";
import { CategorySelection, CategorySkipOption, PreviewBarOption, SponsorTime, StorageChangesObject, UnEncodedSegmentTimes as UnencodedSegmentTimes } from "./types";
import Utils from "./utils";
const utils = new Utils();
interface SBConfig {
userID: string,
/** Contains unsubmitted segments that the user has created. */
segmentTimes: SBMap<string, SponsorTime[]>,
defaultCategory: string,
whitelistedChannels: string[],
forceChannelCheck: boolean,
skipKeybind: string,
startSponsorKeybind: string,
submitKeybind: string,
minutesSaved: number,
@@ -17,7 +19,6 @@ interface SBConfig {
sponsorTimesContributed: number,
submissionCountSinceCategories: number, // New count used to show the "Read The Guidelines!!" message
showTimeWithSkips: boolean,
unsubmittedWarning: boolean,
disableSkipping: boolean,
trackViewCount: boolean,
dontShowNotice: boolean,
@@ -34,8 +35,8 @@ interface SBConfig {
audioNotificationOnSkip,
checkForUnlistedVideos: boolean,
testingServer: boolean,
hashPrefix: boolean,
refetchWhenNotFound: boolean,
ytInfoPermissionGranted: boolean,
// What categories should be skipped
categorySelections: CategorySelection[],
@@ -55,9 +56,7 @@ interface SBConfig {
"preview-selfpromo": PreviewBarOption,
"music_offtopic": PreviewBarOption,
"preview-music_offtopic": PreviewBarOption,
},
hasShownYouCapNotice: boolean
}
}
export interface SBObject {
@@ -67,7 +66,7 @@ export interface SBObject {
config: SBConfig;
// Functions
encodeStoredItem<T>(data: T): T | Array<any>;
encodeStoredItem<T>(data: T): T | UnencodedSegmentTimes;
convertJSON(): void;
}
@@ -145,6 +144,7 @@ const Config: SBObject = {
defaultCategory: "chooseACategory",
whitelistedChannels: [],
forceChannelCheck: false,
skipKeybind: "Enter",
startSponsorKeybind: ";",
submitKeybind: "'",
minutesSaved: 0,
@@ -152,7 +152,6 @@ const Config: SBObject = {
sponsorTimesContributed: 0,
submissionCountSinceCategories: 0,
showTimeWithSkips: true,
unsubmittedWarning: true,
disableSkipping: false,
trackViewCount: true,
dontShowNotice: false,
@@ -169,8 +168,8 @@ const Config: SBObject = {
audioNotificationOnSkip: false,
checkForUnlistedVideos: false,
testingServer: false,
hashPrefix: false,
refetchWhenNotFound: true,
ytInfoPermissionGranted: false,
categorySelections: [{
name: "sponsor",
@@ -231,9 +230,7 @@ const Config: SBObject = {
color: "#a6634a",
opacity: "0.7"
}
},
hasShownYouCapNotice: false
}
},
localConfig: null,
config: null,
@@ -251,10 +248,10 @@ const Config: SBObject = {
*
* @param data
*/
function encodeStoredItem<T>(data: T): T | Array<any> {
function encodeStoredItem<T>(data: T): T | UnencodedSegmentTimes {
// if data is SBMap convert to json for storing
if(!(data instanceof SBMap)) return data;
return Array.from(data.entries());
return Array.from(data.entries()).filter((element) => element[1].length > 0); // Remove empty entries
}
/**
@@ -269,7 +266,7 @@ function decodeStoredItem<T>(id: string, data: T): T | SBMap<string, SponsorTime
if (Config.defaults[id] instanceof SBMap) {
try {
if (!Array.isArray(data)) return data;
return new SBMap(id, data);
return new SBMap(id, data as UnencodedSegmentTimes);
} catch(e) {
console.error("Failed to parse SBMap: " + id);
}
@@ -399,7 +396,7 @@ function migrateOldFormats(config: SBConfig) {
// Migrate old "sponsorTimes"
if (config["sponsorTimes"]) {
let jsonData: any = config["sponsorTimes"];
let jsonData: unknown = config["sponsorTimes"];
// Check if data is stored in the old format for SBMap (a JSON string)
if (typeof jsonData === "string") {
@@ -413,7 +410,7 @@ function migrateOldFormats(config: SBConfig) {
// Otherwise junk data
if (Array.isArray(jsonData)) {
const oldMap = new Map(jsonData);
oldMap.forEach((sponsorTimes: number[][], key) => {
oldMap.forEach((sponsorTimes: [number, number][], key) => {
const segmentTimes: SponsorTime[] = [];
for (const segment of sponsorTimes) {
segmentTimes.push({

View File

@@ -1,6 +1,6 @@
import Config from "./config";
import { SponsorTime, CategorySkipOption, VideoID, SponsorHideType, FetchResponse, VideoInfo, StorageChangesObject } from "./types";
import { SponsorTime, CategorySkipOption, VideoID, SponsorHideType, FetchResponse, VideoInfo, StorageChangesObject, ChannelIDInfo, ChannelIDStatus } from "./types";
import { ContentContainer } from "./types";
import Utils from "./utils";
@@ -19,16 +19,17 @@ utils.wait(() => Config.config !== null, 5000, 10).then(addCSS);
//was sponsor data found when doing SponsorsLookup
let sponsorDataFound = false;
let previousVideoID: VideoID = null;
//the actual sponsorTimes if loaded and UUIDs associated with them
let sponsorTimes: SponsorTime[] = null;
//what video id are these sponsors for
let sponsorVideoID: VideoID = null;
// List of open skip notices
const skipNotices: SkipNotice[] = [];
// JSON video info
let videoInfo: VideoInfo = null;
//the channel this video is about
let channelID: string;
let channelIDInfo: ChannelIDInfo;
// Skips are scheduled to ensure precision.
// Skips are rescheduled every seeking event.
@@ -36,11 +37,13 @@ let channelID: string;
let currentSkipSchedule: NodeJS.Timeout = null;
let seekListenerSetUp = false
/** @type {Array[boolean]} Has the sponsor been skipped */
/** Has the sponsor been skipped */
let sponsorSkipped: boolean[] = [];
//the video
let video: HTMLVideoElement;
// List of videos that have had event listeners added to them
const videoRootsWithEventListeners: HTMLDivElement[] = [];
let onInvidious;
let onMobileYouTube;
@@ -68,8 +71,11 @@ let channelWhitelisted = false;
// create preview bar
let previewBar: PreviewBar = null;
//the player controls on the YouTube player
let controls = null;
/** Element containing the player controls on the YouTube player. */
let controls: HTMLElement | null = null;
/** Contains buttons created by `createButton()`. */
const playerButtons: Record<string, {button: HTMLButtonElement, image: HTMLImageElement}> = {};
// Direct Links after the config is loaded
utils.wait(() => Config.config !== null, 1000, 1).then(() => videoIDChange(getYouTubeVideoID(document.URL)));
@@ -78,10 +84,7 @@ utils.wait(() => Config.config !== null, 1000, 1).then(() => videoIDChange(getYo
//this only happens if there is an error
let sponsorLookupRetries = 0;
//if showing the start sponsor button or the end sponsor button on the player
let showingStartSponsor = true;
//the sponsor times being prepared to be submitted
/** Segments created by the user which have not yet been submitted. */
let sponsorTimesSubmitting: SponsorTime[] = [];
//becomes true when isInfoFound is called
@@ -100,6 +103,7 @@ const skipNoticeContentContainer: ContentContainer = () => ({
unskipSponsorTime,
sponsorTimes,
sponsorTimesSubmitting,
skipNotices,
v: video,
sponsorVideoID,
reskipSponsorTime,
@@ -107,12 +111,15 @@ const skipNoticeContentContainer: ContentContainer = () => ({
onMobileYouTube,
sponsorSubmissionNotice: submissionNotice,
resetSponsorSubmissionNotice,
changeStartSponsorButton,
updateEditButtonsOnPlayer,
previewTime,
videoInfo,
getRealCurrentTime: getRealCurrentTime
});
// value determining when to count segment as skipped and send telemetry to server (percent based)
const manualSkipPercentCount = 0.5;
//get messages from the background script and the popup
chrome.runtime.onMessage.addListener(messageListener);
@@ -123,11 +130,11 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
videoIDChange(getYouTubeVideoID(document.URL));
break;
case "sponsorStart":
sponsorMessageStarted(sendResponse);
startOrEndTimingNewSegment()
break;
case "sponsorDataChanged":
updateSponsorTimesSubmitting();
sendResponse({
creatingSegment: isSegmentCreationInProgress(),
});
break;
case "isInfoFound":
@@ -146,13 +153,14 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
break;
case "getVideoID":
sendResponse({
videoID: sponsorVideoID
videoID: sponsorVideoID,
creatingSegment: isSegmentCreationInProgress(),
});
break;
case "getChannelID":
sendResponse({
channelID: channelID
channelID: channelIDInfo.id
});
break;
@@ -166,10 +174,6 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
channelWhitelisted = request.value;
sponsorsLookup(sponsorVideoID);
break;
case "changeStartSponsorButton":
changeStartSponsorButton(request.showStartSponsor, request.uploadButtonVisible);
break;
case "submitTimes":
submitSponsorTimes();
@@ -198,28 +202,6 @@ if (!Config.configListeners.includes(contentConfigUpdateListener)) {
Config.configListeners.push(contentConfigUpdateListener);
}
//check for hotkey pressed
document.onkeydown = function(e: KeyboardEvent){
const key = e.key;
const video = document.getElementById("movie_player");
const startSponsorKey = Config.config.startSponsorKeybind;
const submitKey = Config.config.submitKeybind;
//is the video in focus, otherwise they could be typing a comment
if (document.activeElement === video) {
if(key == startSponsorKey){
//semicolon
startSponsorClicked();
} else if (key == submitKey) {
//single quote
submitSponsorTimes();
}
}
}
function resetValues() {
lastCheckTime = 0;
lastCheckVideoTime = -1;
@@ -230,7 +212,10 @@ function resetValues() {
videoInfo = null;
channelWhitelisted = false;
channelID = null;
channelIDInfo = {
status: ChannelIDStatus.Fetching,
id: null
};
//empty the preview bar
if (previewBar !== null) {
@@ -268,25 +253,25 @@ async function videoIDChange(id) {
// Wait for options to be ready
await utils.wait(() => Config.config !== null, 5000, 1);
// Get new video info
getVideoInfo();
// If enabled, it will check if this video is private or unlisted and double check with the user if the sponsors should be looked up
if (Config.config.checkForUnlistedVideos) {
try {
await utils.wait(() => !!videoInfo, 5000, 1);
} catch (err) {
alert(chrome.i18n.getMessage("adblockerIssue"));
}
if (isUnlisted()) {
const shouldContinue = confirm(chrome.i18n.getMessage("confirmPrivacy"));
if(!shouldContinue) return;
const shouldContinue = confirm("SponsorBlock: You have the setting 'Ignore Unlisted/Private Videos' enabled."
+ " Due to a change in how segment fetching works, this setting is not needed anymore as it cannot leak your video ID to the server."
+ " It instead sends just the first 4 characters of a longer hash of the videoID to the server, and filters through a subset of the database."
+ " More info about this implementation can be found here: https://github.com/ajayyy/SponsorBlockServer/issues/25"
+ "\n\nPlease click okay to confirm that you acknowledge this and continue using SponsorBlock.");
if (shouldContinue) {
Config.config.checkForUnlistedVideos = false;
} else {
return;
}
}
// Get new video info
getVideoInfo();
// Update whitelist data when the video data is loaded
utils.wait(() => !!videoInfo, 5000, 10).then(whitelistCheck);
whitelistCheck();
//setup the preview bar
if (previewBar === null) {
@@ -310,54 +295,22 @@ async function videoIDChange(id) {
}
}
//warn them if they had unsubmitted times
if (previousVideoID != null) {
//get the sponsor times from storage
const sponsorTimes = Config.config.segmentTimes.get(previousVideoID);
if (sponsorTimes != undefined && sponsorTimes.length > 0 && new URL(document.URL).host !== "music.youtube.com") {
//warn them that they have unsubmitted sponsor times
chrome.runtime.sendMessage({
message: "alertPrevious",
previousVideoID: previousVideoID
});
}
//set the previous video id to the currentID
previousVideoID = id;
} else {
//set the previous id now, don't wait for chrome.storage.get
previousVideoID = id;
}
//close popup
closeInfoMenu();
sponsorsLookup(id);
//make sure everything is properly added
updateVisibilityOfPlayerControlsButton().then(() => {
//see if the onvideo control image needs to be changed
const segments = Config.config.segmentTimes.get(sponsorVideoID);
if (segments != null && segments.length > 0 && segments[segments.length - 1].segment.length >= 2) {
changeStartSponsorButton(true, true);
} else if (segments != null && segments.length > 0 && segments[segments.length - 1].segment.length < 2) {
changeStartSponsorButton(false, true);
} else {
changeStartSponsorButton(true, false);
}
});
// Make sure all player buttons are properly added
updateVisibilityOfPlayerControlsButton();
//reset sponsor times submitting
// Clear unsubmitted segments from the previous video
sponsorTimesSubmitting = [];
updateSponsorTimesSubmitting();
//see if video controls buttons should be added
if (!onInvidious) {
updateVisibilityOfPlayerControlsButton();
}
}
function handleMobileControlsMutations(): void {
updateVisibilityOfPlayerControlsButton();
if (previewBar !== null) {
if (document.body.contains(previewBar.container)) {
const progressBarBackground = document.querySelector<HTMLElement>(".progress-bar-background");
@@ -442,7 +395,7 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
if (video.paused) return;
if (Config.config.disableSkipping || channelWhitelisted || (channelID === null && Config.config.forceChannelCheck)){
if (Config.config.disableSkipping || channelWhitelisted || (channelIDInfo.status === ChannelIDStatus.Fetching && Config.config.forceChannelCheck)){
return;
}
@@ -532,6 +485,8 @@ async function sponsorsLookup(id: string) {
return;
}
addHotkeyListener();
if (!durationListenerSetUp) {
durationListenerSetUp = true;
@@ -608,33 +563,22 @@ async function sponsorsLookup(id: string) {
}
// Check for hashPrefix setting
let getRequest;
if (Config.config.hashPrefix) {
const hashPrefix = (await utils.getHash(id, 1)).substr(0, 4);
getRequest = utils.asyncRequestToServer('GET', "/api/skipSegments/" + hashPrefix, {
categories
});
} else {
getRequest = utils.asyncRequestToServer('GET', "/api/skipSegments", {
videoID: id,
categories
});
}
getRequest.then(async (response: FetchResponse) => {
const hashPrefix = (await utils.getHash(id, 1)).substr(0, 4);
utils.asyncRequestToServer('GET', "/api/skipSegments/" + hashPrefix, {
categories
}).then(async (response: FetchResponse) => {
if (response?.ok) {
let result = JSON.parse(response.responseText);
if (Config.config.hashPrefix) {
result = result.filter((video) => video.videoID === id);
if (result.length > 0) {
result = result[0].segments;
if (result.length === 0) { // return if no segments found
retryFetch(id);
return;
}
} else { // return if no video found
result = result.filter((video) => video.videoID === id);
if (result.length > 0) {
result = result[0].segments;
if (result.length === 0) { // return if no segments found
retryFetch(id);
return;
}
} else { // return if no video found
retryFetch(id);
return;
}
const recievedSegments: SponsorTime[] = result;
@@ -682,12 +626,12 @@ async function sponsorsLookup(id: string) {
sponsorLookupRetries = 0;
} else if (response?.status === 404) {
retryFetch(id);
} else if (sponsorLookupRetries < 90 && !recheckStarted) {
} else if (sponsorLookupRetries < 15 && !recheckStarted) {
recheckStarted = true;
//TODO lower when server becomes better (back to 1 second)
//some error occurred, try again in a second
setTimeout(() => sponsorsLookup(id), 10000 + Math.random() * 30000);
setTimeout(() => sponsorsLookup(id), 5000 + Math.random() * 15000 + 5000 * sponsorLookupRetries);
sponsorLookupRetries++;
}
@@ -705,8 +649,7 @@ function retryFetch(id: string): void {
//if less than 3 days old
if (Date.now() - new Date(dateUploaded).getTime() < 259200000) {
//TODO lower when server becomes better
setTimeout(() => sponsorsLookup(id), 120000);
setTimeout(() => sponsorsLookup(id), 30000 + Math.random() * 90000);
}
});
@@ -755,6 +698,7 @@ async function getVideoInfo(): Promise<void> {
const decodedData = decodeURIComponent(result.responseText).match(/player_response=([^&]*)/)[1];
if (!decodedData) {
console.error("[SB] Failed at getting video info from YouTube.");
console.error("[SB] Data returned from YouTube: " + result.responseText);
return;
}
@@ -858,23 +802,34 @@ function updatePreviewBar(): void {
}
//checks if this channel is whitelisted, should be done only after the channelID has been loaded
function whitelistCheck() {
channelID = videoInfo?.videoDetails?.channelId;
if (!channelID) {
channelID = null;
async function whitelistCheck() {
const whitelistedChannels = Config.config.whitelistedChannels;
const 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
if (channelID) {
channelIDInfo = {
status: ChannelIDStatus.Found,
id: channelID
}
} else {
channelIDInfo = {
status: ChannelIDStatus.Failed,
id: null
}
return;
}
//see if this is a whitelisted channel
const whitelistedChannels = Config.config.whitelistedChannels;
if (whitelistedChannels != undefined && whitelistedChannels.includes(channelID)) {
channelWhitelisted = true;
}
// check if the start of segments were missed
if (Config.config.forceChannelCheck && sponsorTimes && sponsorTimes.length > 0) startSkipScheduleCheckingForStartSponsors();
if (Config.config.forceChannelCheck && sponsorTimes?.length > 0) startSkipScheduleCheckingForStartSponsors();
}
/**
@@ -998,6 +953,26 @@ function previewTime(time: number, unpause = true) {
}
}
//send telemetry and count skip
function sendTelemetryAndCount(skippingSegments: SponsorTime[], secondsSkipped: number, fullSkip: boolean) {
if (!Config.config.trackViewCount) return;
let counted = false;
for (const segment of skippingSegments) {
const index = sponsorTimes.indexOf(segment);
if (index !== -1 && !sponsorSkipped[index]) {
sponsorSkipped[index] = true;
if (!counted) {
Config.config.minutesSaved = Config.config.minutesSaved + secondsSkipped / 60;
Config.config.skipCount = Config.config.skipCount + 1;
counted = true;
}
if (fullSkip) utils.asyncRequestToServer("POST", "/api/viewedVideoSponsorTime?UUID=" + segment.UUID);
}
}
}
//skip from the start time to the end time for a certain index sponsor time
function skipToTime(v: HTMLVideoElement, skipTime: number[], skippingSegments: SponsorTime[], openNotice: boolean) {
// There will only be one submission if it is manual skip
@@ -1016,34 +991,12 @@ function skipToTime(v: HTMLVideoElement, skipTime: number[], skippingSegments: S
if (openNotice) {
//send out the message saying that a sponsor message was skipped
if (!Config.config.dontShowNotice || !autoSkip) {
new SkipNotice(skippingSegments, autoSkip, skipNoticeContentContainer);
skipNotices.push(new SkipNotice(skippingSegments, autoSkip, skipNoticeContentContainer));
}
}
//send telemetry that a this sponsor was skipped
if (Config.config.trackViewCount && autoSkip) {
let alreadySkipped = false;
let isPreviewSegment = false;
for (const segment of skippingSegments) {
const index = sponsorTimes.indexOf(segment);
if (index !== -1 && !sponsorSkipped[index]) {
utils.asyncRequestToServer("POST", "/api/viewedVideoSponsorTime?UUID=" + segment.UUID);
sponsorSkipped[index] = true;
} else if (sponsorSkipped[index]) {
alreadySkipped = true;
}
if (index === -1) isPreviewSegment = true;
}
// Count this as a skip
if (!alreadySkipped && !isPreviewSegment) {
Config.config.minutesSaved = Config.config.minutesSaved + (skipTime[1] - skipTime[0]) / 60;
Config.config.skipCount = Config.config.skipCount + 1;
}
}
if (autoSkip) sendTelemetryAndCount(skippingSegments, skipTime[1] - skipTime[0], true);
}
function unskipSponsorTime(segment: SponsorTime) {
@@ -1054,13 +1007,18 @@ function unskipSponsorTime(segment: SponsorTime) {
}
function reskipSponsorTime(segment: SponsorTime) {
const skippedTime = Math.max(segment.segment[1] - video.currentTime, 0);
const segmentDuration = segment.segment[1] - segment.segment[0];
const fullSkip = skippedTime / segmentDuration > manualSkipPercentCount;
video.currentTime = segment.segment[1];
sendTelemetryAndCount([segment], skippedTime, fullSkip);
startSponsorSchedule(true, segment.segment[1], false);
}
function createButton(baseID, title, callback, imageName, isDraggable=false): boolean {
if (document.getElementById(baseID + "Button") != null) return false;
function createButton(baseID: string, title: string, callback: () => void, imageName: string, isDraggable = false): HTMLElement {
const existingElement = document.getElementById(baseID + "Button");
if (existingElement !== null) return existingElement;
// Button HTML
const newButton = document.createElement("button");
@@ -1084,9 +1042,15 @@ function createButton(baseID, title, callback, imageName, isDraggable=false): bo
newButton.appendChild(newButtonImage);
// Add the button to player
controls.prepend(newButton);
if (controls) controls.prepend(newButton);
return true;
// Store the elements to prevent unnecessary querying
playerButtons[baseID] = {
button: newButton,
image: newButtonImage,
};
return newButton;
}
function getControls(): HTMLElement | false {
@@ -1096,8 +1060,8 @@ function getControls(): HTMLElement | false {
// Mobile YouTube
".player-controls-top",
// Invidious/videojs video element's controls element
".vjs-control-bar"
]
".vjs-control-bar",
];
for (const controlsSelector of controlsSelectors) {
const controls = document.querySelectorAll(controlsSelector);
@@ -1110,53 +1074,75 @@ function getControls(): HTMLElement | false {
return false;
}
//adds all the player controls buttons
async function createButtons(): Promise<boolean> {
/** Creates any missing buttons on the YouTube player if possible. */
async function createButtons(): Promise<void> {
if (onMobileYouTube) return;
const result = await utils.wait(getControls).catch();
//set global controls variable
controls = result;
let createdButton = false;
controls = await utils.wait(getControls).catch();
// Add button if does not already exist in html
createdButton = createButton("startSponsor", "sponsorStart", startSponsorClicked, "PlayerStartIconSponsorBlocker256px.png") || createdButton;
createdButton = createButton("info", "openPopup", openInfoMenu, "PlayerInfoIconSponsorBlocker256px.png") || createdButton;
createdButton = createButton("delete", "clearTimes", clearSponsorTimes, "PlayerDeleteIconSponsorBlocker256px.png") || createdButton;
createdButton = createButton("submit", "SubmitTimes", submitSponsorTimes, "PlayerUploadIconSponsorBlocker256px.png") || createdButton;
return createdButton;
createButton("startSegment", "sponsorStart", () => closeInfoMenuAnd(() => startOrEndTimingNewSegment()), "PlayerStartIconSponsorBlocker.svg");
createButton("cancelSegment", "sponsorCancel", () => closeInfoMenuAnd(() => cancelCreatingSegment()), "PlayerCancelSegmentIconSponsorBlocker.svg");
createButton("info", "openPopup", openInfoMenu, "PlayerInfoIconSponsorBlocker.svg");
createButton("delete", "clearTimes", () => closeInfoMenuAnd(() => clearSponsorTimes()), "PlayerDeleteIconSponsorBlocker.svg");
createButton("submit", "SubmitTimes", submitSponsorTimes, "PlayerUploadIconSponsorBlocker.svg");
}
//adds or removes the player controls button to what it should be
async function updateVisibilityOfPlayerControlsButton(): Promise<boolean> {
//not on a proper video yet
if (!sponsorVideoID) return false;
/** Creates any missing buttons on the player and updates their visiblity. */
async function updateVisibilityOfPlayerControlsButton(): Promise<void> {
// Not on a proper video yet
if (!sponsorVideoID) return;
const createdButtons = await createButtons();
if (!createdButtons) return;
await createButtons();
if (Config.config.hideVideoPlayerControls || onInvidious) {
document.getElementById("startSponsorButton").style.display = "none";
document.getElementById("submitButton").style.display = "none";
} else {
document.getElementById("startSponsorButton").style.removeProperty("display");
}
updateEditButtonsOnPlayer();
//don't show the info button on embeds
// Don't show the info button on embeds
if (Config.config.hideInfoButtonPlayerControls || document.URL.includes("/embed/") || onInvidious) {
document.getElementById("infoButton").style.display = "none";
playerButtons.info.button.style.display = "none";
} else {
document.getElementById("infoButton").style.removeProperty("display");
playerButtons.info.button.style.removeProperty("display");
}
if (Config.config.hideDeleteButtonPlayerControls || onInvidious) {
document.getElementById("deleteButton").style.display = "none";
}
/** Updates the visibility of buttons on the player related to creating segments. */
function updateEditButtonsOnPlayer(): void {
// Don't try to update the buttons if we aren't on a YouTube video page
if (!sponsorVideoID) return;
const buttonsEnabled = !Config.config.hideVideoPlayerControls && !onInvidious;
let creatingSegment = false;
let submitButtonVisible = false;
let deleteButtonVisible = false;
// Only check if buttons should be visible if they're enabled
if (buttonsEnabled) {
creatingSegment = isSegmentCreationInProgress();
// Show only if there are any segments to submit
submitButtonVisible = sponsorTimesSubmitting.length > 1 || (sponsorTimesSubmitting.length > 0 && !creatingSegment);
// Show only if there are any segments to delete
deleteButtonVisible = sponsorTimesSubmitting.length > 1 || (sponsorTimesSubmitting.length > 0 && !creatingSegment);
}
return createdButtons;
// Update the elements
playerButtons.startSegment.button.style.display = buttonsEnabled ? "unset" : "none";
playerButtons.cancelSegment.button.style.display = buttonsEnabled && creatingSegment ? "unset" : "none";
if (buttonsEnabled) {
if (creatingSegment) {
playerButtons.startSegment.image.src = chrome.extension.getURL("icons/PlayerStopIconSponsorBlocker.svg");
playerButtons.startSegment.button.setAttribute("title", chrome.i18n.getMessage("sponsorEnd"));
} else {
playerButtons.startSegment.image.src = chrome.extension.getURL("icons/PlayerStartIconSponsorBlocker.svg");
playerButtons.startSegment.button.setAttribute("title", chrome.i18n.getMessage("sponsorStart"));
}
}
playerButtons.submit.button.style.display = submitButtonVisible && !Config.config.hideUploadButtonPlayerControls ? "unset" : "none";
playerButtons.delete.button.style.display = deleteButtonVisible && !Config.config.hideDeleteButtonPlayerControls ? "unset" : "none";
}
/**
@@ -1177,30 +1163,50 @@ function getRealCurrentTime(): number {
}
}
function startSponsorClicked() {
//it can't update to this info yet
closeInfoMenu();
toggleStartSponsorButton();
//add to sponsorTimes
if (sponsorTimesSubmitting.length > 0 && sponsorTimesSubmitting[sponsorTimesSubmitting.length - 1].segment.length < 2) {
//it is an end time
sponsorTimesSubmitting[sponsorTimesSubmitting.length - 1].segment[1] = getRealCurrentTime();
sponsorTimesSubmitting[sponsorTimesSubmitting.length - 1].segment.sort((a, b) => a > b ? 1 : (a < b ? -1 : 0));
} else {
//it is a start time
function startOrEndTimingNewSegment() {
if (!isSegmentCreationInProgress()) {
sponsorTimesSubmitting.push({
segment: [getRealCurrentTime()],
UUID: null,
category: Config.config.defaultCategory
category: Config.config.defaultCategory,
});
} else {
// Finish creating the new segment
const existingSegment = getIncompleteSegment();
const existingTime = existingSegment.segment[0];
const currentTime = getRealCurrentTime();
// Swap timestamps if the user put the segment end before the start
existingSegment.segment = [Math.min(existingTime, currentTime), Math.max(existingTime, currentTime)];
}
//save this info
// Save the newly created segment
Config.config.segmentTimes.set(sponsorVideoID, sponsorTimesSubmitting);
updateSponsorTimesSubmitting(false)
updateEditButtonsOnPlayer();
updateSponsorTimesSubmitting(false);
}
function getIncompleteSegment(): SponsorTime {
return sponsorTimesSubmitting[sponsorTimesSubmitting.length - 1];
}
/** Is the latest submitting segment incomplete */
function isSegmentCreationInProgress(): boolean {
const segment = getIncompleteSegment();
return segment && segment?.segment?.length !== 2;
}
function cancelCreatingSegment() {
if (isSegmentCreationInProgress()) {
sponsorTimesSubmitting.splice(sponsorTimesSubmitting.length - 1, 1);
Config.config.segmentTimes.set(sponsorVideoID, sponsorTimesSubmitting);
if (sponsorTimesSubmitting.length <= 0) resetSponsorSubmissionNotice();
}
updateEditButtonsOnPlayer();
updateSponsorTimesSubmitting(false);
}
function updateSponsorTimesSubmitting(getFromConfig = true) {
@@ -1229,38 +1235,6 @@ function updateSponsorTimesSubmitting(getFromConfig = true) {
}
}
async function changeStartSponsorButton(showStartSponsor: boolean, uploadButtonVisible: boolean): Promise<boolean> {
if(!sponsorVideoID || onMobileYouTube) return false;
//if it isn't visible, there is no data
const shouldHide = (uploadButtonVisible && !(Config.config.hideDeleteButtonPlayerControls || onInvidious)) ? "unset" : "none"
document.getElementById("deleteButton").style.display = shouldHide;
if (showStartSponsor) {
showingStartSponsor = true;
(<HTMLImageElement> document.getElementById("startSponsorImage")).src = chrome.extension.getURL("icons/PlayerStartIconSponsorBlocker256px.png");
document.getElementById("startSponsorButton").setAttribute("title", chrome.i18n.getMessage("sponsorStart"));
if (document.getElementById("startSponsorImage").style.display != "none" && uploadButtonVisible && !Config.config.hideUploadButtonPlayerControls && !onInvidious) {
document.getElementById("submitButton").style.display = "unset";
} else if (!uploadButtonVisible || onInvidious) {
//disable submit button
document.getElementById("submitButton").style.display = "none";
}
} else {
showingStartSponsor = false;
(<HTMLImageElement> document.getElementById("startSponsorImage")).src = chrome.extension.getURL("icons/PlayerStopIconSponsorBlocker256px.png");
document.getElementById("startSponsorButton").setAttribute("title", chrome.i18n.getMessage("sponsorEND"));
//disable submit button
document.getElementById("submitButton").style.display = "none";
}
}
function toggleStartSponsorButton() {
changeStartSponsorButton(!showingStartSponsor, true);
}
function openInfoMenu() {
if (document.getElementById("sponsorBlockPopupContainer") != null) {
//it's already added
@@ -1270,7 +1244,7 @@ function openInfoMenu() {
popupInitialised = false;
//hide info button
document.getElementById("infoButton").style.display = "none";
if (playerButtons.info) playerButtons.info.button.style.display = "none";
sendRequestToCustomServer('GET', chrome.extension.getURL("popup.html"), function(xmlhttp) {
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
@@ -1316,7 +1290,7 @@ function openInfoMenu() {
const settings = <HTMLImageElement> popup.querySelector("#sbPopupIconSettings");
const edit = <HTMLImageElement> popup.querySelector("#sbPopupIconEdit");
const check = <HTMLImageElement> popup.querySelector("#sbPopupIconCheck");
logo.src = chrome.extension.getURL("icons/LogoSponsorBlocker256px.png");
logo.src = chrome.extension.getURL("icons/IconSponsorBlocker256px.png");
settings.src = chrome.extension.getURL("icons/settings.svg");
edit.src = chrome.extension.getURL("icons/pencil.svg");
check.src = chrome.extension.getURL("icons/check.svg");
@@ -1332,20 +1306,28 @@ function openInfoMenu() {
function closeInfoMenu() {
const popup = document.getElementById("sponsorBlockPopupContainer");
if (popup != null) {
popup.remove();
if (popup === null) return;
//show info button if it's not an embed
if (!document.URL.includes("/embed/")) {
document.getElementById("infoButton").style.display = "unset";
}
popup.remove();
// Show info button if it's not an embed
if (!document.URL.includes("/embed/") && playerButtons.info) {
playerButtons.info.button.style.display = "unset";
}
}
function clearSponsorTimes() {
//it can't update to this info yet
/**
* The content script currently has no way to notify the info menu of changes. As a workaround we close it, thus making it query the new information when reopened.
*
* This function and all its uses should be removed when this issue is fixed.
* */
function closeInfoMenuAnd<T>(func: () => T): T {
closeInfoMenu();
return func();
}
function clearSponsorTimes() {
const currentVideoID = sponsorVideoID;
const sponsorTimes = Config.config.segmentTimes.get(currentVideoID);
@@ -1355,6 +1337,8 @@ function clearSponsorTimes() {
+ "\n" + chrome.i18n.getMessage("confirmMSG")
if(!confirm(confirmMessage)) return;
resetSponsorSubmissionNotice();
//clear the sponsor times
Config.config.segmentTimes.delete(currentVideoID);
@@ -1362,9 +1346,7 @@ function clearSponsorTimes() {
sponsorTimesSubmitting = [];
updatePreviewBar();
//set buttons to be correct
changeStartSponsorButton(true, false);
updateEditButtonsOnPlayer();
}
}
@@ -1409,7 +1391,7 @@ function vote(type: number, UUID: string, category?: string, skipNotice?: SkipNo
//success (treat rate limits as a success)
skipNotice.afterVote.bind(skipNotice)(utils.getSponsorTimeFromUUID(sponsorTimes, UUID), type, category);
} else if (response.successType == -1) {
skipNotice.setNoticeInfoMessage.bind(skipNotice)(utils.getErrorMessage(response.statusCode))
skipNotice.setNoticeInfoMessage.bind(skipNotice)(utils.getErrorMessage(response.statusCode, response.responseText))
skipNotice.resetVoteButtonInfo.bind(skipNotice)();
}
}
@@ -1430,31 +1412,17 @@ function dontShowNoticeAgain() {
closeAllSkipNotices();
}
function sponsorMessageStarted(callback: (response: MessageResponse) => void) {
video = document.querySelector('video');
//send back current time
callback({
time: video.currentTime
})
//update button
toggleStartSponsorButton();
}
/**
* Helper method for the submission notice to clear itself when it closes
*/
function resetSponsorSubmissionNotice() {
submissionNotice?.close();
submissionNotice = null;
}
function submitSponsorTimes() {
if (submissionNotice !== null) return;
//it can't update to this info yet
closeInfoMenu();
if (sponsorTimesSubmitting !== undefined && sponsorTimesSubmitting.length > 0) {
submissionNotice = new SubmissionNotice(skipNoticeContentContainer, sendSubmitMessage);
}
@@ -1463,10 +1431,10 @@ function submitSponsorTimes() {
//send the message to the background js
//called after all the checks have been made that it's okay to do so
async function sendSubmitMessage(): Promise<void> {
//add loading animation
(<HTMLImageElement> document.getElementById("submitImage")).src = chrome.extension.getURL("icons/PlayerUploadIconSponsorBlocker256px.png");
document.getElementById("submitButton").style.animation = "rotate 1s 0s infinite";
async function sendSubmitMessage() {
// Add loading animation
playerButtons.submit.image.src = chrome.extension.getURL("icons/PlayerUploadIconSponsorBlocker.svg");
playerButtons.submit.button.style.animation = "rotate 1s 0s infinite";
//check if a sponsor exceeds the duration of the video
for (let i = 0; i < sponsorTimesSubmitting.length; i++) {
@@ -1493,17 +1461,19 @@ async function sendSubmitMessage(): Promise<void> {
const response = await utils.asyncRequestToServer("POST", "/api/skipSegments", {
videoID: sponsorVideoID,
userID: Config.config.userID,
segments: sponsorTimesSubmitting
segments: sponsorTimesSubmitting,
});
if (response.status === 200) {
//hide loading message
const submitButton = document.getElementById("submitButton");
// Handle submission success
const submitButton = playerButtons.submit.button;
// Make the animation finite
submitButton.style.animation = "rotate 1s";
//finish this animation
//when the animation is over, hide the button
const animationEndListener = function() {
changeStartSponsorButton(true, false);
// When the animation is over, hide the button
const animationEndListener = () => {
updateEditButtonsOnPlayer();
submitButton.style.animation = "none";
@@ -1512,13 +1482,11 @@ async function sendSubmitMessage(): Promise<void> {
submitButton.addEventListener("animationend", animationEndListener);
//clear the sponsor times
// Remove segments from storage since they've already been submitted
Config.config.segmentTimes.delete(sponsorVideoID);
//add submissions to current sponsors list
if (sponsorTimes === null) sponsorTimes = [];
sponsorTimes = sponsorTimes.concat(sponsorTimesSubmitting);
// Add submissions to current sponsors list
sponsorTimes = (sponsorTimes || []).concat(sponsorTimesSubmitting);
// Increase contribution count
Config.config.sponsorTimesContributed = Config.config.sponsorTimesContributed + sponsorTimesSubmitting.length;
@@ -1532,11 +1500,11 @@ async function sendSubmitMessage(): Promise<void> {
updatePreviewBar();
} else {
//show that the upload failed
document.getElementById("submitButton").style.animation = "unset";
(<HTMLImageElement> document.getElementById("submitImage")).src = chrome.extension.getURL("icons/PlayerUploadFailedIconSponsorBlocker256px.png");
// Show that the upload failed
playerButtons.submit.button.style.animation = "unset";
playerButtons.submit.image.src = chrome.extension.getURL("icons/PlayerUploadFailedIconSponsorBlocker.svg");
alert(utils.getErrorMessage(response.status) + "\n\n" + (response.responseText));
alert(utils.getErrorMessage(response.status, response.responseText));
}
}
@@ -1562,12 +1530,41 @@ function getSegmentsMessage(sponsorTimes: SponsorTime[]): string {
return sponsorTimesMessage;
}
/**
* Is this an unlisted YouTube video.
* Assumes that the the privacy info is available.
*/
function isUnlisted(): boolean {
return videoInfo?.microformat?.playerMicroformatRenderer?.isUnlisted || videoInfo?.videoDetails?.isPrivate;
function addHotkeyListener(): boolean {
let videoRoot = document.getElementById("movie_player") as HTMLDivElement;
if (onInvidious) videoRoot = (document.getElementById("player-container") ?? document.getElementById("player")) as HTMLDivElement;
if (video.baseURI.startsWith("https://www.youtube.com/tv#/")) videoRoot = document.querySelector("ytlr-watch-page") as HTMLDivElement;
if (videoRoot && !videoRootsWithEventListeners.includes(videoRoot)) {
videoRoot.addEventListener("keydown", hotkeyListener);
videoRootsWithEventListeners.push(videoRoot);
return true;
}
return false;
}
function hotkeyListener(e: KeyboardEvent): void {
const key = e.key;
const skipKey = Config.config.skipKeybind;
const startSponsorKey = Config.config.startSponsorKeybind;
const submitKey = Config.config.submitKeybind;
switch (key) {
case skipKey:
if (skipNotices.length > 0) {
const latestSkipNotice = skipNotices[skipNotices.length - 1];
latestSkipNotice.toggleSkip.call(latestSkipNotice);
}
break;
case startSponsorKey:
startOrEndTimingNewSegment();
break;
case submitKey:
submitSponsorTimes();
break;
}
}
/**
@@ -1645,6 +1642,8 @@ function showTimeWithoutSkips(skippedDuration: number): void {
display.appendChild(duration);
}
const durationAfterSkips = utils.getFormattedTime(video.duration - skippedDuration)
duration.innerText = skippedDuration <= 0 ? "" : " (" + utils.getFormattedTime(video.duration - skippedDuration) + ")";
duration.innerText = (durationAfterSkips == null || skippedDuration <= 0) ? "" : " (" + durationAfterSkips + ")";
}

View File

@@ -12,7 +12,6 @@ interface DefaultMessage {
message:
"update"
| "sponsorStart"
| "sponsorDataChanged"
| "isInfoFound"
| "getVideoID"
| "getChannelID"
@@ -25,13 +24,7 @@ interface BoolValueMessage {
value: boolean;
}
interface ChangeStartSponsorButtonMessage {
message: "changeStartSponsorButton";
showStartSponsor: boolean;
uploadButtonVisible: boolean;
}
export type Message = BaseMessage & (DefaultMessage | BoolValueMessage | ChangeStartSponsorButtonMessage);
export type Message = BaseMessage & (DefaultMessage | BoolValueMessage);
interface IsInfoFoundMessageResponse {
found: boolean;
@@ -47,7 +40,7 @@ interface GetChannelIDResponse {
}
interface SponsorStartResponse {
time: number;
creatingSegment: boolean;
}
interface IsChannelWhitelistedResponse {

View File

@@ -288,7 +288,7 @@ function invidiousInit(checkbox: HTMLInputElement, option: string) {
if (utils.isFirefox()) permissions = [];
chrome.permissions.contains({
origins: utils.getInvidiousInstancesRegex(),
origins: utils.getPermissionRegex(),
permissions: permissions
}, function (result) {
if (result != checkbox.checked) {

35
src/permissions.ts Normal file
View File

@@ -0,0 +1,35 @@
import Config from "./config";
import Utils from "./utils";
const utils = new Utils();
// This is needed, if Config is not imported before Utils, things break.
// Probably due to cyclic dependencies
Config.config;
window.addEventListener('DOMContentLoaded', init);
async function init() {
utils.localizeHtmlPage();
const domains = document.location.hash.replace("#", "").split(",");
const acceptButton = document.getElementById("acceptPermissionButton");
acceptButton.addEventListener("click", () => {
chrome.permissions.request({
origins: utils.getPermissionRegex(domains),
permissions: []
}, (granted) => {
if (granted) {
alert(chrome.i18n.getMessage("permissionRequestSuccess"));
Config.config.ytInfoPermissionGranted = true;
chrome.tabs.getCurrent((tab) => {
chrome.tabs.remove(tab.id);
});
} else {
alert(chrome.i18n.getMessage("permissionRequestFailed"));
}
});
});
}

View File

@@ -126,8 +126,8 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
PageElements.optionsButton.addEventListener("click", openOptions);
PageElements.helpButton.addEventListener("click", openHelp);
//if true, the button now selects the end time
let startTimeChosen = false;
/** If true, the content script is in the process of creating a new segment. */
let creatingSegment = false;
//the start and end time pairs (2d)
let sponsorTimes: SponsorTime[] = [];
@@ -233,11 +233,13 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
function onTabs(tabs) {
messageHandler.sendMessage(tabs[0].id, {message: 'getVideoID'}, function(result) {
if (result != undefined && result.videoID) {
if (result !== undefined && result.videoID) {
currentVideoID = result.videoID;
creatingSegment = result.creatingSegment;
loadTabData(tabs);
} else if (result == undefined && chrome.runtime.lastError) {
// this isn't a YouTube video then, or at least the content script is not loaded
} else if (result === undefined && chrome.runtime.lastError) {
//this isn't a YouTube video then, or at least the content script is not loaded
displayNoVideo();
}
});
@@ -253,19 +255,11 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
//load video times for this video
const sponsorTimesStorage = Config.config.segmentTimes.get(currentVideoID);
if (sponsorTimesStorage != undefined && sponsorTimesStorage.length > 0) {
if (sponsorTimesStorage[sponsorTimesStorage.length - 1] != undefined && sponsorTimesStorage[sponsorTimesStorage.length - 1].segment.length < 2) {
startTimeChosen = true;
PageElements.sponsorStart.innerHTML = chrome.i18n.getMessage("sponsorEnd");
}
sponsorTimes = sponsorTimesStorage;
//show submission section
PageElements.submissionSection.style.display = "unset";
showSubmitTimesIfNecessary();
}
updateSegmentEditingUI();
//check if this video's sponsors are known
messageHandler.sendMessage(
tabs[0].id,
@@ -321,51 +315,44 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
//the content script will get the message if a YouTube page is open
messageHandler.query({
active: true,
currentWindow: true
}, tabs => {
currentWindow: true,
}, (tabs) => {
messageHandler.sendMessage(
tabs[0].id,
{from: 'popup', message: 'sponsorStart'},
startSponsorCallback
async (response) => {
startSponsorCallback(response);
// Perform a second update after the config changes take effect as a workaround for a race condition
const removeListener = (listener: typeof lateUpdate) => {
const index = Config.configListeners.indexOf(listener);
if (index !== -1) Config.configListeners.splice(index, 1);
};
const lateUpdate = () => {
startSponsorCallback(response);
removeListener(lateUpdate);
};
Config.configListeners.push(lateUpdate);
// Remove the listener after 200ms in case the changes were propagated by the time we got the response
setTimeout(() => removeListener(lateUpdate), 200);
},
);
});
}
function startSponsorCallback(response) {
const sponsorTimesIndex = sponsorTimes.length - (startTimeChosen ? 1 : 0);
function startSponsorCallback(response: {creatingSegment: boolean}) {
creatingSegment = response.creatingSegment;
if (sponsorTimes[sponsorTimesIndex] == undefined) {
sponsorTimes[sponsorTimesIndex] = {
segment: [],
category: Config.config.defaultCategory,
UUID: null
};
// Only update the segments after a segment was created
if (!creatingSegment) {
sponsorTimes = Config.config.segmentTimes.get(currentVideoID) || [];
}
sponsorTimes[sponsorTimesIndex].segment[startTimeChosen ? 1 : 0] = response.time;
const localStartTimeChosen = startTimeChosen;
Config.config.segmentTimes.set(currentVideoID, sponsorTimes);
//send a message to the client script
if (localStartTimeChosen) {
messageHandler.query({
active: true,
currentWindow: true
}, tabs => {
messageHandler.sendMessage(
tabs[0].id,
{message: "sponsorDataChanged"}
);
});
}
updateStartTimeChosen();
//show submission section
PageElements.submissionSection.style.display = "unset";
showSubmitTimesIfNecessary();
// Update the UI
updateSegmentEditingUI();
}
//display the video times from the array at the top, in a different section
@@ -400,7 +387,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
extraInfo = " (" + chrome.i18n.getMessage("hiddenDueToDuration") + ")";
}
const segmentTimeFromTo = getFormattedTime(segmentTimes[i].segment[0]) + " " + chrome.i18n.getMessage("to") + " " + getFormattedTime(segmentTimes[i].segment[1]);
const segmentTimeFromTo = utils.getFormattedTime(segmentTimes[i].segment[0], true) + " " + chrome.i18n.getMessage("to") + " " + utils.getFormattedTime(segmentTimes[i].segment[1], true);
const segmentTimeFromToNode = document.createTextNode(segmentTimeFromTo);
const textNode = document.createTextNode(utils.shortCategoryName(segmentTimes[i].category) + extraInfo);
@@ -409,7 +396,6 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
sponsorTimeButton.appendChild(categoryColorCircle);
sponsorTimeButton.appendChild(textNode);
const votingButtons = document.createElement("div");
votingButtons.classList.add("votingButtons");
@@ -431,9 +417,18 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
downvoteButton.src = chrome.extension.getURL("icons/thumbs_down.svg");
downvoteButton.addEventListener("click", () => vote(0, UUID));
//add thumbs up and down buttons to the container
//uuid button
const uuidButton = document.createElement("img");
uuidButton.id = "sponsorTimesCopyUUIDButtonContainer" + UUID;
uuidButton.className = "voteButton";
uuidButton.src = chrome.extension.getURL("icons/clipboard.svg");
uuidButton.addEventListener("click", () => navigator.clipboard.writeText(UUID));
//add thumbs up, thumbs down and uuid copy buttons to the container
voteButtonsContainer.appendChild(upvoteButton);
voteButtonsContainer.appendChild(downvoteButton);
voteButtonsContainer.appendChild(uuidButton);
//add click listener to open up vote panel
sponsorTimeButton.addEventListener("click", function() {
@@ -480,34 +475,13 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
PageElements.showNoticeAgain.style.display = "none";
}
function updateStartTimeChosen() {
//update startTimeChosen letiable
if (!startTimeChosen) {
startTimeChosen = true;
PageElements.sponsorStart.innerHTML = chrome.i18n.getMessage("sponsorEnd");
} else {
resetStartTimeChosen();
}
/** Updates any UI related to segment editing and submission according to the current state. */
function updateSegmentEditingUI() {
PageElements.sponsorStart.innerText = chrome.i18n.getMessage(creatingSegment ? "sponsorEnd" : "sponsorStart");
PageElements.submissionSection.style.display = sponsorTimes && sponsorTimes.length > 0 ? "unset" : "none";
}
//set it to false
function resetStartTimeChosen() {
startTimeChosen = false;
PageElements.sponsorStart.innerHTML = chrome.i18n.getMessage("sponsorStart");
}
//hides and shows the submit times button when needed
function showSubmitTimesIfNecessary() {
//check if an end time has been specified for the latest sponsor time
if (sponsorTimes.length > 0 && sponsorTimes[sponsorTimes.length - 1].segment.length > 1) {
//show submit times button
document.getElementById("submitTimesContainer").style.display = "flex";
} else {
//hide submit times button
document.getElementById("submitTimesContainer").style.display = "none";
}
}
//make the options div visible
function openOptions() {
chrome.runtime.sendMessage({"message": "openConfig"});
@@ -553,7 +527,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
PageElements.sponsorTimesContributionsContainer.classList.remove("hidden");
} else {
PageElements.setUsernameStatus.innerText = utils.getErrorMessage(response.status);
PageElements.setUsernameStatus.innerText = utils.getErrorMessage(response.status, response.responseText);
}
});
@@ -580,41 +554,26 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
function vote(type, UUID) {
//add loading info
addVoteMessage(chrome.i18n.getMessage("Loading"), UUID)
addVoteMessage(chrome.i18n.getMessage("Loading"), UUID);
//send the vote message to the tab
chrome.runtime.sendMessage({
message: "submitVote",
type: type,
UUID: UUID
}, function(response) {
}, function (response) {
if (response != undefined) {
//see if it was a success or failure
if (response.successType == 1 || (response.successType == -1 && response.statusCode == 429)) {
//success (treat rate limits as a success)
addVoteMessage(chrome.i18n.getMessage("voted"), UUID)
addVoteMessage(chrome.i18n.getMessage("voted"), UUID);
} else if (response.successType == -1) {
addVoteMessage(utils.getErrorMessage(response.statusCode), UUID)
addVoteMessage(utils.getErrorMessage(response.statusCode, response.responseText), UUID);
}
}
});
}
//converts time in seconds to minutes:seconds
function getFormattedTime(seconds) {
const minutes = Math.floor(seconds / 60);
const secondsDisplayNumber = Math.round(seconds - minutes * 60);
let secondsDisplay = String(secondsDisplayNumber);
if (secondsDisplayNumber < 10) {
//add a zero
secondsDisplay = "0" + secondsDisplay;
}
const formatted = minutes + ":" + secondsDisplay;
return formatted;
}
function whitelistChannel() {
//get the channel url
messageHandler.query({
@@ -626,8 +585,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
{message: 'getChannelID'},
function(response) {
if (!response.channelID) {
alert(chrome.i18n.getMessage("channelDataNotFound") + "\n\n" +
chrome.i18n.getMessage("itCouldBeAdblockerIssue"));
alert(chrome.i18n.getMessage("channelDataNotFound") + " https://github.com/ajayyy/SponsorBlock/issues/753");
return;
}
@@ -737,10 +695,11 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
* @param {float} seconds
* @returns {string}
*/
function getFormattedHours(minues) {
const hours = Math.floor(minues / 60);
return (hours > 0 ? hours + "h " : "") + (minues % 60).toFixed(1);
}
function getFormattedHours(minutes) {
minutes = Math.round(minutes * 10) / 10
const hours = Math.floor(minutes / 60);
return (hours > 0 ? hours + "h " : "") + (minutes % 60).toFixed(1);
}
//end of function
}

View File

@@ -1,7 +1,7 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import SkipNoticeComponent from "../components/SkipNoticeComponent";
import SkipNoticeComponent, { SkipNoticeAction } from "../components/SkipNoticeComponent";
import { SponsorTime, ContentContainer } from "../types";
class SkipNotice {
@@ -15,6 +15,8 @@ class SkipNotice {
skipNoticeRef: React.MutableRefObject<SkipNoticeComponent>;
constructor(segments: SponsorTime[], autoSkip = false, contentContainer: ContentContainer) {
this.skipNoticeRef = React.createRef();
this.segments = segments;
this.autoSkip = autoSkip;
this.contentContainer = contentContainer;
@@ -67,6 +69,13 @@ class SkipNotice {
ReactDOM.unmountComponentAtNode(this.noticeElement);
this.noticeElement.remove();
const skipNotices = this.contentContainer().skipNotices;
skipNotices.splice(skipNotices.indexOf(this), 1);
}
toggleSkip(): void {
this.skipNoticeRef.current.prepAction(SkipNoticeAction.Unskip);
}
}

View File

@@ -1,13 +1,15 @@
import SubmissionNotice from "./render/SubmissionNotice";
import SkipNoticeComponent from "./components/SkipNoticeComponent";
import SkipNotice from "./render/SkipNotice";
interface ContentContainer {
export interface ContentContainer {
(): {
vote: (type: number, UUID: string, category?: string, skipNotice?: SkipNoticeComponent) => void,
dontShowNoticeAgain: () => void,
unskipSponsorTime: (segment: SponsorTime) => void,
sponsorTimes: SponsorTime[],
sponsorTimesSubmitting: SponsorTime[],
skipNotices: SkipNotice[],
v: HTMLVideoElement,
sponsorVideoID,
reskipSponsorTime: (segment: SponsorTime) => void,
@@ -15,42 +17,42 @@ interface ContentContainer {
onMobileYouTube: boolean,
sponsorSubmissionNotice: SubmissionNotice,
resetSponsorSubmissionNotice: () => void,
changeStartSponsorButton: (showStartSponsor: boolean, uploadButtonVisible: boolean) => Promise<boolean>,
updateEditButtonsOnPlayer: () => void,
previewTime: (time: number, unpause?: boolean) => void,
videoInfo: VideoInfo,
getRealCurrentTime: () => number
}
}
interface FetchResponse {
export interface FetchResponse {
responseText: string,
status: number,
ok: boolean
}
interface VideoDurationResponse {
export interface VideoDurationResponse {
duration: number;
}
enum CategorySkipOption {
export enum CategorySkipOption {
ShowOverlay,
ManualSkip,
AutoSkip
}
interface CategorySelection {
export interface CategorySelection {
name: string;
option: CategorySkipOption
}
enum SponsorHideType {
export enum SponsorHideType {
Visible = undefined,
Downvoted = 1,
MinimumDuration
}
interface SponsorTime {
segment: number[];
export interface SponsorTime {
segment: [number] | [number, number];
UUID: string;
category: string;
@@ -58,13 +60,13 @@ interface SponsorTime {
hidden?: SponsorHideType;
}
interface PreviewBarOption {
export interface PreviewBarOption {
color: string,
opacity: string
}
interface Registration {
export interface Registration {
message: string,
id: string,
allFrames: boolean,
@@ -73,12 +75,12 @@ interface Registration {
matches: string[]
}
interface BackgroundScriptContainer {
export interface BackgroundScriptContainer {
registerFirefoxContentScript: (opts: Registration) => void,
unregisterFirefoxContentScript: (id: string) => void
}
interface VideoInfo {
export interface VideoInfo {
responseContext: {
serviceTrackingParams: Array<{service: string, params: Array<{key: string, value: string}>}>,
webResponseContextExtensionData: {
@@ -154,22 +156,19 @@ interface VideoInfo {
messages: unknown;
}
type VideoID = string;
export type VideoID = string;
type StorageChangesObject = { [key: string]: chrome.storage.StorageChange };
export type StorageChangesObject = { [key: string]: chrome.storage.StorageChange };
export {
FetchResponse,
VideoDurationResponse,
ContentContainer,
CategorySelection,
CategorySkipOption,
SponsorTime,
VideoID,
SponsorHideType,
PreviewBarOption,
Registration,
BackgroundScriptContainer,
VideoInfo,
StorageChangesObject,
};
export type UnEncodedSegmentTimes = [string, SponsorTime[]][];
export enum ChannelIDStatus {
Fetching,
Found,
Failed
}
export interface ChannelIDInfo {
id: string,
status: ChannelIDStatus
}

View File

@@ -3,10 +3,10 @@ import { CategorySelection, SponsorTime, FetchResponse, BackgroundScriptContaine
import * as CompileConfig from "../config.json";
class Utils {
export default class Utils {
// Contains functions needed from the background script
backgroundScriptContainer: BackgroundScriptContainer | null = null;
backgroundScriptContainer: BackgroundScriptContainer | null;
// Used to add content scripts and CSS required
js = [
@@ -19,12 +19,12 @@ class Utils {
"popup.css"
];
constructor(backgroundScriptContainer?: BackgroundScriptContainer) {
constructor(backgroundScriptContainer: BackgroundScriptContainer = null) {
this.backgroundScriptContainer = backgroundScriptContainer;
}
// Function that can be used to wait for a condition before returning
async wait(condition: () => HTMLElement | boolean, timeout = 5000, check = 100): Promise<HTMLElement | boolean> {
/** Function that can be used to wait for a condition before returning. */
async wait<T>(condition: () => T | false, timeout = 5000, check = 100): Promise<T> {
return await new Promise((resolve, reject) => {
setTimeout(() => reject("TIMEOUT"), timeout);
@@ -43,6 +43,12 @@ class Utils {
});
}
containsPermission(permissions: chrome.permissions.Permissions): Promise<boolean> {
return new Promise((resolve) => {
chrome.permissions.contains(permissions, resolve)
});
}
/**
* Asks for the optional permissions required for all extra sites.
* It also starts the content script registrations.
@@ -57,7 +63,7 @@ class Utils {
if (this.isFirefox()) permissions = [];
chrome.permissions.request({
origins: this.getInvidiousInstancesRegex(),
origins: this.getPermissionRegex(),
permissions: permissions
}, async (granted) => {
if (granted) {
@@ -78,7 +84,6 @@ class Utils {
* For now, it is just SB.config.invidiousInstances.
*/
setupExtraSiteContentScripts(): void {
if (this.isFirefox()) {
const firefoxJS = [];
for (const file of this.js) {
@@ -95,7 +100,7 @@ class Utils {
allFrames: true,
js: firefoxJS,
css: firefoxCSS,
matches: this.getInvidiousInstancesRegex()
matches: this.getPermissionRegex()
};
if (this.backgroundScriptContainer) {
@@ -106,7 +111,7 @@ class Utils {
} else {
chrome.declarativeContent.onPageChanged.removeRules(["invidious"], () => {
const conditions = [];
for (const regex of this.getInvidiousInstancesRegex()) {
for (const regex of this.getPermissionRegex()) {
conditions.push(new chrome.declarativeContent.PageStateMatcher({
pageUrl: { urlMatches: regex }
}));
@@ -149,7 +154,7 @@ class Utils {
}
chrome.permissions.remove({
origins: this.getInvidiousInstancesRegex()
origins: this.getPermissionRegex()
});
}
@@ -250,16 +255,20 @@ class Utils {
}
/**
* @returns {String[]} Invidious Instances in regex form
* @returns {String[]} Domains in regex form
*/
getInvidiousInstancesRegex(): string[] {
const invidiousInstancesRegex: string[] = [];
for (const url of Config.config.invidiousInstances) {
invidiousInstancesRegex.push("https://*." + url + "/*");
invidiousInstancesRegex.push("http://*." + url + "/*");
getPermissionRegex(domains: string[] = []): string[] {
const permissionRegex: string[] = [];
if (domains.length === 0) {
domains = [...Config.config.invidiousInstances];
}
return invidiousInstancesRegex;
for (const url of domains) {
permissionRegex.push("https://*." + url + "/*");
permissionRegex.push("http://*." + url + "/*");
}
return permissionRegex;
}
generateUserID(length = 36): string {
@@ -286,10 +295,11 @@ class Utils {
* @param {int} statusCode
* @returns {string} errorMessage
*/
getErrorMessage(statusCode: number): string {
getErrorMessage(statusCode: number, responseText: string): string {
let errorMessage = "";
const postFix = (responseText ? "\n\n" + responseText : "");
if([400, 429, 409, 502, 0].includes(statusCode)) {
if([400, 429, 409, 502, 503, 0].includes(statusCode)) {
//treat them the same
if (statusCode == 503) statusCode = 502;
@@ -299,7 +309,7 @@ class Utils {
errorMessage = chrome.i18n.getMessage("connectionError") + statusCode;
}
return errorMessage;
return errorMessage + postFix;
}
/**
@@ -375,6 +385,9 @@ class Utils {
//add a zero
minutesDisplay = "0" + minutesDisplay;
}
if (isNaN(hours) || isNaN(minutes)) {
return null;
}
const formatted = (hours ? hours + ":" : "") + minutesDisplay + ":" + secondsDisplay;
@@ -430,5 +443,3 @@ class Utils {
}
}
export default Utils;