Merge pull request #923 from ajayyy/silent-skip

Mute segments
This commit is contained in:
Ajay Ramachandran 2021-09-02 13:22:44 -04:00 committed by GitHub
commit ef942fca8e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 318 additions and 94 deletions

View file

@ -52,6 +52,9 @@
"reskip": { "reskip": {
"message": "Reskip" "message": "Reskip"
}, },
"unmute": {
"message": "Unmute"
},
"paused": { "paused": {
"message": "Paused" "message": "Paused"
}, },
@ -302,6 +305,9 @@
"skip": { "skip": {
"message": "Skip" "message": "Skip"
}, },
"mute": {
"message": "Mute"
},
"skip_category": { "skip_category": {
"message": "Skip {0}?" "message": "Skip {0}?"
}, },
@ -604,6 +610,9 @@
"autoSkipOnMusicVideos": { "autoSkipOnMusicVideos": {
"message": "Auto skip all segments when there is a non-music segment" "message": "Auto skip all segments when there is a non-music segment"
}, },
"muteSegments": {
"message": "Allow segments that mute audio instead of skip"
},
"colorFormatIncorrect": { "colorFormatIncorrect": {
"message": "Your color is formatted incorrectly. It should be a 3 or 6 digit hex code with a number sign at the beginning." "message": "Your color is formatted incorrectly. It should be a 3 or 6 digit hex code with a number sign at the beginning."
}, },

View file

@ -472,7 +472,7 @@ input::-webkit-inner-spin-button {
text-decoration: underline; text-decoration: underline;
} }
.sponsorTimeCategories { .sponsorTimeEditSelector {
margin-top: 5px; margin-top: 5px;
margin-bottom: 5px; margin-bottom: 5px;

View file

@ -50,6 +50,22 @@
<br/> <br/>
</div> </div>
<div option-type="toggle" sync-option="muteSegments">
<label class="switch-container">
<label class="switch">
<input type="checkbox" checked>
<span class="slider round"></span>
</label>
<div class="switch-label">
__MSG_muteSegments__
</div>
</label>
<br/>
<br/>
<br/>
</div>
<br/> <br/>
<br/> <br/>

View file

@ -1,7 +1,7 @@
import * as React from "react"; import * as React from "react";
import * as CompileConfig from "../../config.json"; import * as CompileConfig from "../../config.json";
import Config from "../config" import Config from "../config"
import { Category, ContentContainer, CategoryActionType, SponsorHideType, SponsorTime, NoticeVisbilityMode } from "../types"; import { Category, ContentContainer, CategoryActionType, SponsorHideType, SponsorTime, NoticeVisbilityMode, ActionType } from "../types";
import NoticeComponent from "./NoticeComponent"; import NoticeComponent from "./NoticeComponent";
import NoticeTextSelectionComponent from "./NoticeTextSectionComponent"; import NoticeTextSelectionComponent from "./NoticeTextSectionComponent";
@ -39,8 +39,9 @@ export interface SkipNoticeState {
maxCountdownTime?: () => number; maxCountdownTime?: () => number;
countdownText?: string; countdownText?: string;
unskipText?: string; skipButtonText?: string;
unskipCallback?: (index: number) => void; skipButtonCallback?: (index: number) => void;
showSkipButton?: boolean;
downvoting?: boolean; downvoting?: boolean;
choosingCategory?: boolean; choosingCategory?: boolean;
@ -110,8 +111,9 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
countdownTime: Config.config.skipNoticeDuration, countdownTime: Config.config.skipNoticeDuration,
countdownText: null, countdownText: null,
unskipText: chrome.i18n.getMessage("unskip"), skipButtonText: this.getUnskipText(),
unskipCallback: (index) => this.unskip(index), skipButtonCallback: (index) => this.unskip(index),
showSkipButton: true,
downvoting: false, downvoting: false,
choosingCategory: false, choosingCategory: false,
@ -126,7 +128,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
if (!this.autoSkip) { if (!this.autoSkip) {
// Assume manual skip is only skipping 1 submission // Assume manual skip is only skipping 1 submission
Object.assign(this.state, this.getUnskippedModeInfo(0, chrome.i18n.getMessage("skip"))); Object.assign(this.state, this.getUnskippedModeInfo(0, this.getSkipText()));
} }
} }
@ -297,9 +299,9 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
} }
getSkipButton(): JSX.Element { getSkipButton(): JSX.Element {
if (this.segments.length > 1 if (this.state.showSkipButton && (this.segments.length > 1
|| getCategoryActionType(this.segments[0].category) !== CategoryActionType.POI || getCategoryActionType(this.segments[0].category) !== CategoryActionType.POI
|| this.props.unskipTime) { || this.props.unskipTime)) {
return ( return (
<span className="sponsorSkipNoticeUnskipSection"> <span className="sponsorSkipNoticeUnskipSection">
<button id={"sponsorSkipUnskipButton" + this.idSuffix} <button id={"sponsorSkipUnskipButton" + this.idSuffix}
@ -307,7 +309,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
style={{marginLeft: "4px"}} style={{marginLeft: "4px"}}
onClick={() => this.prepAction(SkipNoticeAction.Unskip)}> onClick={() => this.prepAction(SkipNoticeAction.Unskip)}>
{this.state.unskipText + (this.state.showKeybindHint ? " (" + Config.config.skipKeybind + ")" : "")} {this.state.skipButtonText + (this.state.showKeybindHint ? " (" + Config.config.skipKeybind + ")" : "")}
</button> </button>
</span> </span>
); );
@ -397,7 +399,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
this.contentContainer().vote(undefined, this.segments[index].UUID, this.categoryOptionRef.current.value as Category, this) this.contentContainer().vote(undefined, this.segments[index].UUID, this.categoryOptionRef.current.value as Category, this)
break; break;
case SkipNoticeAction.Unskip: case SkipNoticeAction.Unskip:
this.state.unskipCallback(index); this.state.skipButtonCallback(index);
break; break;
} }
@ -457,7 +459,29 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
unskip(index: number): void { unskip(index: number): void {
this.contentContainer().unskipSponsorTime(this.segments[index], this.props.unskipTime); this.contentContainer().unskipSponsorTime(this.segments[index], this.props.unskipTime);
this.unskippedMode(index, chrome.i18n.getMessage("reskip")); this.unskippedMode(index, this.getReskipText());
}
reskip(index: number): void {
this.contentContainer().reskipSponsorTime(this.segments[index]);
const newState: SkipNoticeState = {
skipButtonText: this.getUnskipText(),
skipButtonCallback: this.unskip.bind(this),
maxCountdownTime: () => Config.config.skipNoticeDuration,
countdownTime: Config.config.skipNoticeDuration
};
// See if the title should be changed
if (!this.autoSkip) {
newState.noticeTitle = chrome.i18n.getMessage("noticeTitle");
}
//reset countdown
this.setState(newState, () => {
this.noticeRef.current.resetCountdown();
});
} }
/** Sets up notice to be not skipped yet */ /** Sets up notice to be not skipped yet */
@ -479,36 +503,14 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
} : this.state.maxCountdownTime; } : this.state.maxCountdownTime;
return { return {
unskipText: buttonText, skipButtonText: buttonText,
unskipCallback: (index) => this.reskip(index), skipButtonCallback: (index) => this.reskip(index),
// change max duration to however much of the sponsor is left // change max duration to however much of the sponsor is left
maxCountdownTime: maxCountdownTime, maxCountdownTime: maxCountdownTime,
countdownTime: maxCountdownTime() countdownTime: maxCountdownTime()
} as SkipNoticeState; } as SkipNoticeState;
} }
reskip(index: number): void {
this.contentContainer().reskipSponsorTime(this.segments[index]);
const newState: SkipNoticeState = {
unskipText: chrome.i18n.getMessage("unskip"),
unskipCallback: this.unskip.bind(this),
maxCountdownTime: () => Config.config.skipNoticeDuration,
countdownTime: Config.config.skipNoticeDuration
};
// See if the title should be changed
if (!this.autoSkip) {
newState.noticeTitle = chrome.i18n.getMessage("noticeTitle");
}
//reset countdown
this.setState(newState, () => {
this.noticeRef.current.resetCountdown();
});
}
afterVote(segment: SponsorTime, type: number, category: Category): void { afterVote(segment: SponsorTime, type: number, category: Category): void {
this.addVoteButtonInfo(chrome.i18n.getMessage("voted")); this.addVoteButtonInfo(chrome.i18n.getMessage("voted"));
@ -559,6 +561,52 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
this.props.closeListener(); this.props.closeListener();
} }
unmutedListener(): void {
if (this.props.segments.length === 1
&& this.props.segments[0].actionType === ActionType.Mute
&& this.contentContainer().v.currentTime >= this.props.segments[0].segment[1]) {
this.setState({
showSkipButton: false
});
}
}
private getUnskipText(): string {
switch (this.props.segments[0].actionType) {
case ActionType.Mute: {
return chrome.i18n.getMessage("unmute");
}
case ActionType.Skip:
default: {
return chrome.i18n.getMessage("unskip");
}
}
}
private getReskipText(): string {
switch (this.props.segments[0].actionType) {
case ActionType.Mute: {
return chrome.i18n.getMessage("mute");
}
case ActionType.Skip:
default: {
return chrome.i18n.getMessage("reskip");
}
}
}
private getSkipText(): string {
switch (this.props.segments[0].actionType) {
case ActionType.Mute: {
return chrome.i18n.getMessage("mute");
}
case ActionType.Skip:
default: {
return chrome.i18n.getMessage("skip");
}
}
}
} }
export default SkipNoticeComponent; export default SkipNoticeComponent;

View file

@ -1,12 +1,12 @@
import * as React from "react"; import * as React from "react";
import Config from "../config";
import * as CompileConfig from "../../config.json"; import * as CompileConfig from "../../config.json";
import Config from "../config";
import { ActionType, ActionTypes, Category, CategoryActionType, ContentContainer, SponsorTime } from "../types";
import Utils from "../utils"; import Utils from "../utils";
import { Category, CategoryActionType, ContentContainer, SponsorTime } from "../types";
import SubmissionNoticeComponent from "./SubmissionNoticeComponent";
import { getCategoryActionType } from "../utils/categoryUtils"; import { getCategoryActionType } from "../utils/categoryUtils";
import SubmissionNoticeComponent from "./SubmissionNoticeComponent";
const utils = new Utils(); const utils = new Utils();
export interface SponsorTimeEditProps { export interface SponsorTimeEditProps {
@ -32,6 +32,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
idSuffix: string; idSuffix: string;
categoryOptionRef: React.RefObject<HTMLSelectElement>; categoryOptionRef: React.RefObject<HTMLSelectElement>;
actionTypeOptionRef: React.RefObject<HTMLSelectElement>;
configUpdateListener: () => void; configUpdateListener: () => void;
@ -39,6 +40,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
super(props); super(props);
this.categoryOptionRef = React.createRef(); this.categoryOptionRef = React.createRef();
this.actionTypeOptionRef = React.createRef();
this.idSuffix = this.props.idSuffix; this.idSuffix = this.props.idSuffix;
@ -171,7 +173,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
{/* Category */} {/* Category */}
<div style={{position: "relative"}}> <div style={{position: "relative"}}>
<select id={"sponsorTimeCategories" + this.idSuffix} <select id={"sponsorTimeCategories" + this.idSuffix}
className="sponsorTimeCategories" className="sponsorTimeEditSelector sponsorTimeCategories"
defaultValue={sponsorTime.category} defaultValue={sponsorTime.category}
ref={this.categoryOptionRef} ref={this.categoryOptionRef}
onChange={this.categorySelectionChange.bind(this)}> onChange={this.categorySelectionChange.bind(this)}>
@ -188,6 +190,19 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
</a> </a>
</div> </div>
{/* Action Type */}
{getCategoryActionType(sponsorTime.category) === CategoryActionType.Skippable ? (
<div style={{position: "relative"}}>
<select id={"sponsorTimeActionTypes" + this.idSuffix}
className="sponsorTimeEditSelector sponsorTimeActionTypes"
defaultValue={sponsorTime.actionType}
ref={this.actionTypeOptionRef}
onChange={() => this.saveEditTimes()}>
{this.getActionTypeOptions()}
</select>
</div>
): ""}
<br/> <br/>
{/* Editing Tools */} {/* Editing Tools */}
@ -269,6 +284,21 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
this.saveEditTimes(); this.saveEditTimes();
} }
getActionTypeOptions(): React.ReactElement[] {
const elements = [];
for (const actionType of ActionTypes) {
elements.push(
<option value={actionType}
key={actionType}>
{chrome.i18n.getMessage(actionType)}
</option>
);
}
return elements;
}
setTimeToNow(index: number): void { setTimeToNow(index: number): void {
this.setTimeTo(index, this.props.contentContainer().getRealCurrentTime()); this.setTimeTo(index, this.props.contentContainer().getRealCurrentTime());
} }
@ -331,6 +361,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
} }
sponsorTimesSubmitting[this.props.index].category = this.categoryOptionRef.current.value as Category; sponsorTimesSubmitting[this.props.index].category = this.categoryOptionRef.current.value as Category;
sponsorTimesSubmitting[this.props.index].actionType =
this.actionTypeOptionRef?.current ? this.actionTypeOptionRef.current.value as ActionType : ActionType.Skip;
Config.config.segmentTimes.set(this.props.contentContainer().sponsorVideoID, sponsorTimesSubmitting); Config.config.segmentTimes.set(this.props.contentContainer().sponsorVideoID, sponsorTimesSubmitting);

View file

@ -17,6 +17,7 @@ interface SBConfig {
submissionCountSinceCategories: number, // New count used to show the "Read The Guidelines!!" message submissionCountSinceCategories: number, // New count used to show the "Read The Guidelines!!" message
showTimeWithSkips: boolean, showTimeWithSkips: boolean,
disableSkipping: boolean, disableSkipping: boolean,
muteSegments: boolean,
trackViewCount: boolean, trackViewCount: boolean,
trackViewCountInPrivate: boolean, trackViewCountInPrivate: boolean,
dontShowNotice: boolean, dontShowNotice: boolean,
@ -162,6 +163,7 @@ const Config: SBObject = {
submissionCountSinceCategories: 0, submissionCountSinceCategories: 0,
showTimeWithSkips: true, showTimeWithSkips: true,
disableSkipping: false, disableSkipping: false,
muteSegments: true,
trackViewCount: true, trackViewCount: true,
trackViewCountInPrivate: true, trackViewCountInPrivate: true,
dontShowNotice: false, dontShowNotice: false,

View file

@ -1,5 +1,5 @@
import Config from "./config"; import Config from "./config";
import { SponsorTime, CategorySkipOption, VideoID, SponsorHideType, VideoInfo, StorageChangesObject, CategoryActionType, ChannelIDInfo, ChannelIDStatus, SponsorSourceType, SegmentUUID, Category, SkipToTimeParams, ToggleSkippable } from "./types"; import { SponsorTime, CategorySkipOption, VideoID, SponsorHideType, VideoInfo, StorageChangesObject, CategoryActionType, ChannelIDInfo, ChannelIDStatus, SponsorSourceType, SegmentUUID, Category, SkipToTimeParams, ToggleSkippable, ActionType, ScheduledTime } from "./types";
import { ContentContainer } from "./types"; import { ContentContainer } from "./types";
import Utils from "./utils"; import Utils from "./utils";
@ -45,6 +45,7 @@ let sponsorSkipped: boolean[] = [];
//the video //the video
let video: HTMLVideoElement; let video: HTMLVideoElement;
let videoMuted = false; // Has it been attempted to be muted
let videoMutationObserver: MutationObserver = null; let videoMutationObserver: MutationObserver = null;
// List of videos that have had event listeners added to them // List of videos that have had event listeners added to them
const videosWithEventListeners: HTMLVideoElement[] = []; const videosWithEventListeners: HTMLVideoElement[] = [];
@ -396,7 +397,6 @@ function cancelSponsorSchedule(): void {
} }
/** /**
*
* @param currentTime Optional if you don't want to use the actual current time * @param currentTime Optional if you don't want to use the actual current time
*/ */
function startSponsorSchedule(includeIntersectingSegments = false, currentTime?: number, includeNonIntersectingSegments = true): void { function startSponsorSchedule(includeIntersectingSegments = false, currentTime?: number, includeNonIntersectingSegments = true): void {
@ -412,6 +412,17 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
} }
if (!video || video.paused) return; if (!video || video.paused) return;
if (currentTime === undefined || currentTime === null) currentTime = video.currentTime;
if (videoMuted && !inMuteSegment(currentTime)) {
video.muted = false;
videoMuted = false;
for (const notice of skipNotices) {
// So that the notice can hide buttons
notice.unmutedListener();
}
}
if (Config.config.disableSkipping || channelWhitelisted || (channelIDInfo.status === ChannelIDStatus.Fetching && Config.config.forceChannelCheck)){ if (Config.config.disableSkipping || channelWhitelisted || (channelIDInfo.status === ChannelIDStatus.Fetching && Config.config.forceChannelCheck)){
return; return;
@ -419,14 +430,12 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
if (incorrectVideoCheck()) return; if (incorrectVideoCheck()) return;
if (currentTime === undefined || currentTime === null) currentTime = video.currentTime;
const skipInfo = getNextSkipIndex(currentTime, includeIntersectingSegments, includeNonIntersectingSegments); const skipInfo = getNextSkipIndex(currentTime, includeIntersectingSegments, includeNonIntersectingSegments);
if (skipInfo.index === -1) return; if (skipInfo.index === -1) return;
const currentSkip = skipInfo.array[skipInfo.index]; const currentSkip = skipInfo.array[skipInfo.index];
const skipTime: number[] = [currentSkip.segment[0], skipInfo.array[skipInfo.endIndex].segment[1]]; const skipTime: number[] = [currentSkip.scheduledTime, skipInfo.array[skipInfo.endIndex].segment[1]];
const timeUntilSponsor = skipTime[0] - currentTime; const timeUntilSponsor = skipTime[0] - currentTime;
const videoID = sponsorVideoID; const videoID = sponsorVideoID;
@ -461,7 +470,8 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
openNotice: skipInfo.openNotice openNotice: skipInfo.openNotice
}); });
if (utils.getCategorySelection(currentSkip.category)?.option === CategorySkipOption.ManualSkip) { if (utils.getCategorySelection(currentSkip.category)?.option === CategorySkipOption.ManualSkip
|| currentSkip.actionType === ActionType.Mute) {
forcedSkipTime = skipTime[0] + 0.001; forcedSkipTime = skipTime[0] + 0.001;
} else { } else {
forcedSkipTime = skipTime[1]; forcedSkipTime = skipTime[1];
@ -480,12 +490,19 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
} }
} }
function inMuteSegment(currentTime: number): boolean {
const checkFunction = (segment) => segment.actionType === ActionType.Mute && segment.segment[0] <= currentTime && segment.segment[1] > currentTime;
return sponsorTimes?.some(checkFunction) || sponsorTimesSubmitting.some(checkFunction);
}
/** /**
* This makes sure the videoID is still correct and if the sponsorTime is included * This makes sure the videoID is still correct and if the sponsorTime is included
*/ */
function incorrectVideoCheck(videoID?: string, sponsorTime?: SponsorTime): boolean { function incorrectVideoCheck(videoID?: string, sponsorTime?: SponsorTime): boolean {
const currentVideoID = getYouTubeVideoID(document.URL); const currentVideoID = getYouTubeVideoID(document.URL);
if (currentVideoID !== (videoID || sponsorVideoID) || (sponsorTime && (!sponsorTimes || !sponsorTimes.includes(sponsorTime)) && !sponsorTimesSubmitting.includes(sponsorTime))) { if (currentVideoID !== (videoID || sponsorVideoID) || (sponsorTime
&& (!sponsorTimes || !sponsorTimes?.some((time) => time.segment === sponsorTime.segment))
&& !sponsorTimesSubmitting.some((time) => time.segment === sponsorTime.segment))) {
// Something has really gone wrong // Something has really gone wrong
console.error("[SponsorBlock] The videoID recorded when trying to skip is different than what it should be."); console.error("[SponsorBlock] The videoID recorded when trying to skip is different than what it should be.");
console.error("[SponsorBlock] VideoID recorded: " + sponsorVideoID + ". Actual VideoID: " + currentVideoID); console.error("[SponsorBlock] VideoID recorded: " + sponsorVideoID + ". Actual VideoID: " + currentVideoID);
@ -575,7 +592,7 @@ function setupVideoListeners() {
} }
if (!Config.config.dontShowNotice) { if (!Config.config.dontShowNotice) {
const currentPoiSegment = sponsorTimes.find((segment) => const currentPoiSegment = sponsorTimes?.find((segment) =>
getCategoryActionType(segment.category) === CategoryActionType.POI && getCategoryActionType(segment.category) === CategoryActionType.POI &&
video.currentTime - segment.segment[0] > 0 && video.currentTime - segment.segment[0] > 0 &&
video.currentTime - segment.segment[0] < previewBar.getMinimumSize(true)); video.currentTime - segment.segment[0] < previewBar.getMinimumSize(true));
@ -644,6 +661,7 @@ async function sponsorsLookup(id: string, keepOldSubmissions = true) {
const hashPrefix = (await utils.getHash(id, 1)).substr(0, 4); const hashPrefix = (await utils.getHash(id, 1)).substr(0, 4);
const response = await utils.asyncRequestToServer('GET', "/api/skipSegments/" + hashPrefix, { const response = await utils.asyncRequestToServer('GET', "/api/skipSegments/" + hashPrefix, {
categories, categories,
actionTypes: Config.config.muteSegments ? [ActionType.Skip, ActionType.Mute] : [ActionType.Skip],
userAgent: `${chrome.runtime.id}` userAgent: `${chrome.runtime.id}`
}); });
@ -945,31 +963,33 @@ async function whitelistCheck() {
* Returns info about the next upcoming sponsor skip * Returns info about the next upcoming sponsor skip
*/ */
function getNextSkipIndex(currentTime: number, includeIntersectingSegments: boolean, includeNonIntersectingSegments: boolean): function getNextSkipIndex(currentTime: number, includeIntersectingSegments: boolean, includeNonIntersectingSegments: boolean):
{array: SponsorTime[], index: number, endIndex: number, openNotice: boolean} { {array: ScheduledTime[], index: number, endIndex: number, openNotice: boolean} {
const sponsorStartTimes = getStartTimes(sponsorTimes, includeIntersectingSegments, includeNonIntersectingSegments); const { includedTimes: submittedArray, startTimeIndexes: sponsorStartTimes } =
const sponsorStartTimesAfterCurrentTime = getStartTimes(sponsorTimes, includeIntersectingSegments, includeNonIntersectingSegments, currentTime, true, true); getStartTimes(sponsorTimes, includeIntersectingSegments, includeNonIntersectingSegments);
const { startTimeIndexes: sponsorStartTimesAfterCurrentTime } = getStartTimes(sponsorTimes, includeIntersectingSegments, includeNonIntersectingSegments, currentTime, true, true);
const minSponsorTimeIndex = sponsorStartTimes.indexOf(Math.min(...sponsorStartTimesAfterCurrentTime)); const minSponsorTimeIndex = sponsorStartTimes.indexOf(Math.min(...sponsorStartTimesAfterCurrentTime));
const endTimeIndex = getLatestEndTimeIndex(sponsorTimes, minSponsorTimeIndex); const endTimeIndex = getLatestEndTimeIndex(submittedArray, minSponsorTimeIndex);
const unsubmittedSponsorStartTimes = getStartTimes(sponsorTimesSubmitting, includeIntersectingSegments, includeNonIntersectingSegments); const { includedTimes: unsubmittedArray, startTimeIndexes: unsubmittedSponsorStartTimes } =
const unsubmittedSponsorStartTimesAfterCurrentTime = getStartTimes(sponsorTimesSubmitting, includeIntersectingSegments, includeNonIntersectingSegments, currentTime, false, false); getStartTimes(sponsorTimesSubmitting, includeIntersectingSegments, includeNonIntersectingSegments);
const { startTimeIndexes: unsubmittedSponsorStartTimesAfterCurrentTime } = getStartTimes(sponsorTimesSubmitting, includeIntersectingSegments, includeNonIntersectingSegments, currentTime, false, false);
const minUnsubmittedSponsorTimeIndex = unsubmittedSponsorStartTimes.indexOf(Math.min(...unsubmittedSponsorStartTimesAfterCurrentTime)); const minUnsubmittedSponsorTimeIndex = unsubmittedSponsorStartTimes.indexOf(Math.min(...unsubmittedSponsorStartTimesAfterCurrentTime));
const previewEndTimeIndex = getLatestEndTimeIndex(sponsorTimesSubmitting, minUnsubmittedSponsorTimeIndex); const previewEndTimeIndex = getLatestEndTimeIndex(unsubmittedArray, minUnsubmittedSponsorTimeIndex);
if ((minUnsubmittedSponsorTimeIndex === -1 && minSponsorTimeIndex !== -1) || if ((minUnsubmittedSponsorTimeIndex === -1 && minSponsorTimeIndex !== -1) ||
sponsorStartTimes[minSponsorTimeIndex] < unsubmittedSponsorStartTimes[minUnsubmittedSponsorTimeIndex]) { sponsorStartTimes[minSponsorTimeIndex] < unsubmittedSponsorStartTimes[minUnsubmittedSponsorTimeIndex]) {
return { return {
array: sponsorTimes.filter((segment) => getCategoryActionType(segment.category) === CategoryActionType.Skippable), array: submittedArray,
index: minSponsorTimeIndex, index: minSponsorTimeIndex,
endIndex: endTimeIndex, endIndex: endTimeIndex,
openNotice: true openNotice: true
}; };
} else { } else {
return { return {
array: sponsorTimesSubmitting.filter((segment) => getCategoryActionType(segment.category) === CategoryActionType.Skippable), array: unsubmittedArray,
index: minUnsubmittedSponsorTimeIndex, index: minUnsubmittedSponsorTimeIndex,
endIndex: previewEndTimeIndex, endIndex: previewEndTimeIndex,
openNotice: false openNotice: false
@ -993,7 +1013,10 @@ function getNextSkipIndex(currentTime: number, includeIntersectingSegments: bool
function getLatestEndTimeIndex(sponsorTimes: SponsorTime[], index: number, hideHiddenSponsors = true): number { function getLatestEndTimeIndex(sponsorTimes: SponsorTime[], index: number, hideHiddenSponsors = true): number {
// Only combine segments for AutoSkip // Only combine segments for AutoSkip
if (index == -1 || if (index == -1 ||
!shouldAutoSkip(sponsorTimes[index])) return index; !shouldAutoSkip(sponsorTimes[index])
|| sponsorTimes[index].actionType !== ActionType.Skip) {
return index;
}
// Default to the normal endTime // Default to the normal endTime
let latestEndTimeIndex = index; let latestEndTimeIndex = index;
@ -1004,7 +1027,8 @@ function getLatestEndTimeIndex(sponsorTimes: SponsorTime[], index: number, hideH
if (currentSegment[0] <= latestEndTime && currentSegment[1] > latestEndTime if (currentSegment[0] <= latestEndTime && currentSegment[1] > latestEndTime
&& (!hideHiddenSponsors || sponsorTimes[i].hidden === SponsorHideType.Visible) && (!hideHiddenSponsors || sponsorTimes[i].hidden === SponsorHideType.Visible)
&& shouldAutoSkip(sponsorTimes[i])) { && shouldAutoSkip(sponsorTimes[i])
&& sponsorTimes[i].actionType === ActionType.Skip) {
// Overlapping segment // Overlapping segment
latestEndTimeIndex = i; latestEndTimeIndex = i;
} }
@ -1029,24 +1053,43 @@ function getLatestEndTimeIndex(sponsorTimes: SponsorTime[], index: number, hideH
* the current time, but end after * the current time, but end after
*/ */
function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments: boolean, includeNonIntersectingSegments: boolean, function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments: boolean, includeNonIntersectingSegments: boolean,
minimum?: number, onlySkippableSponsors = false, hideHiddenSponsors = false): number[] { minimum?: number, onlySkippableSponsors = false, hideHiddenSponsors = false): {includedTimes: ScheduledTime[], startTimeIndexes: number[]} {
if (sponsorTimes === null) return []; if (!sponsorTimes) return {includedTimes: [], startTimeIndexes: []};
const startTimes: number[] = []; const includedTimes: ScheduledTime[] = [];
const startTimeIndexes: number[] = [];
for (let i = 0; i < sponsorTimes?.length; i++) { const possibleTimes = sponsorTimes.flatMap((sponsorTime) => {
const results = [{
...sponsorTime,
scheduledTime: sponsorTime.segment[0]
}]
if (sponsorTime.actionType === ActionType.Mute) {
// Schedule at the end time to know when to unmute
results.push({
...sponsorTime,
scheduledTime: sponsorTime.segment[1]
})
}
return results;
})
for (let i = 0; i < possibleTimes.length; i++) {
if ((minimum === undefined if ((minimum === undefined
|| ((includeNonIntersectingSegments && sponsorTimes[i].segment[0] >= minimum) || ((includeNonIntersectingSegments && possibleTimes[i].scheduledTime >= minimum)
|| (includeIntersectingSegments && sponsorTimes[i].segment[0] < minimum && sponsorTimes[i].segment[1] > minimum))) || (includeIntersectingSegments && possibleTimes[i].scheduledTime < minimum && possibleTimes[i].segment[1] > minimum)))
&& (!onlySkippableSponsors || shouldSkip(sponsorTimes[i])) && (!onlySkippableSponsors || shouldSkip(possibleTimes[i]))
&& (!hideHiddenSponsors || sponsorTimes[i].hidden === SponsorHideType.Visible) && (!hideHiddenSponsors || possibleTimes[i].hidden === SponsorHideType.Visible)
&& getCategoryActionType(sponsorTimes[i].category) === CategoryActionType.Skippable) { && getCategoryActionType(possibleTimes[i].category) === CategoryActionType.Skippable) {
startTimes.push(sponsorTimes[i].segment[0]); startTimeIndexes.push(possibleTimes[i].scheduledTime);
includedTimes.push(possibleTimes[i]);
} }
} }
return startTimes; return { includedTimes, startTimeIndexes };
} }
/** /**
@ -1092,13 +1135,27 @@ function skipToTime({v, skipTime, skippingSegments, openNotice, forceAutoSkip, u
if ((autoSkip || sponsorTimesSubmitting.some((time) => time.segment === skippingSegments[0].segment)) if ((autoSkip || sponsorTimesSubmitting.some((time) => time.segment === skippingSegments[0].segment))
&& v.currentTime !== skipTime[1]) { && v.currentTime !== skipTime[1]) {
// Fix for looped videos not working when skipping to the end #426 switch(skippingSegments[0].actionType) {
// for some reason you also can't skip to 1 second before the end case ActionType.Skip: {
if (v.loop && v.duration > 1 && skipTime[1] >= v.duration - 1) { // Fix for looped videos not working when skipping to the end #426
v.currentTime = 0; // for some reason you also can't skip to 1 second before the end
} else { if (v.loop && v.duration > 1 && skipTime[1] >= v.duration - 1) {
v.currentTime = skipTime[1]; v.currentTime = 0;
} else {
v.currentTime = skipTime[1];
}
break;
}
case ActionType.Mute: {
if (!v.muted) {
v.muted = true;
videoMuted = true;
}
break;
}
} }
} }
if (!autoSkip if (!autoSkip
@ -1138,19 +1195,29 @@ function skipToTime({v, skipTime, skippingSegments, openNotice, forceAutoSkip, u
} }
function unskipSponsorTime(segment: SponsorTime, unskipTime: number = null) { function unskipSponsorTime(segment: SponsorTime, unskipTime: number = null) {
//add a tiny bit of time to make sure it is not skipped again if (segment.actionType === ActionType.Mute) {
console.log(unskipTime) video.muted = false;
video.currentTime = unskipTime ?? segment.segment[0] + 0.001; videoMuted = false;
} else {
//add a tiny bit of time to make sure it is not skipped again
video.currentTime = unskipTime ?? segment.segment[0] + 0.001;
}
} }
function reskipSponsorTime(segment: SponsorTime) { function reskipSponsorTime(segment: SponsorTime) {
const skippedTime = Math.max(segment.segment[1] - video.currentTime, 0); if (segment.actionType === ActionType.Mute) {
const segmentDuration = segment.segment[1] - segment.segment[0]; video.muted = true;
const fullSkip = skippedTime / segmentDuration > manualSkipPercentCount; videoMuted = true;
} else {
video.currentTime = segment.segment[1]; const skippedTime = Math.max(segment.segment[1] - video.currentTime, 0);
sendTelemetryAndCount([segment], skippedTime, fullSkip); const segmentDuration = segment.segment[1] - segment.segment[0];
startSponsorSchedule(true, segment.segment[1], false); const fullSkip = skippedTime / segmentDuration > manualSkipPercentCount;
video.currentTime = segment.segment[1];
sendTelemetryAndCount([segment], skippedTime, fullSkip);
startSponsorSchedule(true, segment.segment[1], false);
}
} }
function createButton(baseID: string, title: string, callback: () => void, imageName: string, isDraggable = false): HTMLElement { function createButton(baseID: string, title: string, callback: () => void, imageName: string, isDraggable = false): HTMLElement {
@ -1193,13 +1260,13 @@ function createButton(baseID: string, title: string, callback: () => void, image
function shouldAutoSkip(segment: SponsorTime): boolean { function shouldAutoSkip(segment: SponsorTime): boolean {
return utils.getCategorySelection(segment.category)?.option === CategorySkipOption.AutoSkip || return utils.getCategorySelection(segment.category)?.option === CategorySkipOption.AutoSkip ||
(Config.config.autoSkipOnMusicVideos && sponsorTimes.some((s) => s.category === "music_offtopic") (Config.config.autoSkipOnMusicVideos && sponsorTimes?.some((s) => s.category === "music_offtopic")
&& getCategoryActionType(segment.category) === CategoryActionType.Skippable); && getCategoryActionType(segment.category) === CategoryActionType.Skippable);
} }
function shouldSkip(segment: SponsorTime): boolean { function shouldSkip(segment: SponsorTime): boolean {
return utils.getCategorySelection(segment.category)?.option !== CategorySkipOption.ShowOverlay || return utils.getCategorySelection(segment.category)?.option !== CategorySkipOption.ShowOverlay ||
(Config.config.autoSkipOnMusicVideos && sponsorTimes.some((s) => s.category === "music_offtopic")); (Config.config.autoSkipOnMusicVideos && sponsorTimes?.some((s) => s.category === "music_offtopic"));
} }
function getControls(): HTMLElement | false { function getControls(): HTMLElement | false {
@ -1333,6 +1400,7 @@ function startOrEndTimingNewSegment() {
segment: [getRealCurrentTime()], segment: [getRealCurrentTime()],
UUID: null, UUID: null,
category: Config.config.defaultCategory, category: Config.config.defaultCategory,
actionType: ActionType.Skip,
source: SponsorSourceType.Local source: SponsorSourceType.Local
}); });
} else { } else {
@ -1389,6 +1457,7 @@ function updateSponsorTimesSubmitting(getFromConfig = true) {
segment: segmentTime.segment, segment: segmentTime.segment,
UUID: segmentTime.UUID, UUID: segmentTime.UUID,
category: segmentTime.category, category: segmentTime.category,
actionType: segmentTime.actionType,
source: segmentTime.source source: segmentTime.source
}); });
} }

View file

@ -71,6 +71,10 @@ class SkipNotice {
toggleSkip(): void { toggleSkip(): void {
this.skipNoticeRef?.current?.prepAction(SkipNoticeAction.Unskip); this.skipNoticeRef?.current?.prepAction(SkipNoticeAction.Unskip);
} }
unmutedListener(): void {
this.skipNoticeRef?.current?.unmutedListener();
}
} }
export default SkipNotice; export default SkipNotice;

View file

@ -56,6 +56,13 @@ export enum CategoryActionType {
POI = "_POI" POI = "_POI"
} }
export enum ActionType {
Skip = "skip",
Mute = "mute"
}
export const ActionTypes = [ActionType.Skip, ActionType.Mute];
export type SegmentUUID = string & { __segmentUUIDBrand: unknown }; export type SegmentUUID = string & { __segmentUUIDBrand: unknown };
export type Category = string & { __categoryBrand: unknown }; export type Category = string & { __categoryBrand: unknown };
@ -69,11 +76,16 @@ export interface SponsorTime {
UUID: SegmentUUID; UUID: SegmentUUID;
category: Category; category: Category;
actionType: ActionType;
hidden?: SponsorHideType; hidden?: SponsorHideType;
source?: SponsorSourceType; source?: SponsorSourceType;
} }
export interface ScheduledTime extends SponsorTime {
scheduledTime: number;
}
export interface PreviewBarOption { export interface PreviewBarOption {
color: string, color: string,
opacity: string opacity: string

View file

@ -13,8 +13,11 @@ test("Selenium Chrome test", async () => {
await createSegment(driver, "4", "10.33", "0:04.000 to 0:10.330"); await createSegment(driver, "4", "10.33", "0:04.000 to 0:10.330");
await editSegments(driver, 0, "0:04.000", "0:10.330", "5", "13.211", "0:05.000 to 0:13.211", false); await editSegments(driver, 0, "0:04.000", "0:10.330", "5", "13.211", "0:05.000 to 0:13.211", false);
await autoskipSegment(driver, 5, 13.211); await autoskipSegment(driver, 5, 13.211);
await setSegmentActionType(driver, 0, 1, false);
await editSegments(driver, 0, "0:05.000", "0:13.211", "5", "7.5", "0:05.000 to 0:07.500", false);
await muteSkipSegment(driver, 5, 7.5);
} finally { } finally {
await driver.quit(); await driver.quit();
} }
@ -24,7 +27,7 @@ async function setup(): Promise<WebDriver> {
const options = new Chrome.Options(); const options = new Chrome.Options();
options.addArguments("--load-extension=" + Path.join(__dirname, "../dist/")); options.addArguments("--load-extension=" + Path.join(__dirname, "../dist/"));
options.addArguments("--mute-audio"); options.addArguments("--mute-audio");
options.addArguments("--disable-features=PreloadMediaEngagementData, MediaEngagementBypassAutoplayPolicies") options.addArguments("--disable-features=PreloadMediaEngagementData, MediaEngagementBypassAutoplayPolicies");
const driver = await new Builder().forBrowser("chrome").setChromeOptions(options).build(); const driver = await new Builder().forBrowser("chrome").setChromeOptions(options).build();
driver.manage().setTimeouts({ driver.manage().setTimeouts({
@ -106,6 +109,16 @@ async function editSegments(driver: WebDriver, index: number, expectedStartTimeB
await driver.wait(until.elementTextIs(sponsorTimeDisplay, expectedDisplayedTime)); await driver.wait(until.elementTextIs(sponsorTimeDisplay, expectedDisplayedTime));
} }
async function setSegmentActionType(driver: WebDriver, index: number, actionTypeIndex: number, openSubmitBox: boolean): Promise<void> {
if (openSubmitBox) {
const submitButton = await driver.findElement(By.id("submitButton"));
await submitButton.click();
}
const actionTypeSelection = await driver.findElement(By.css(`#sponsorTimeActionTypesSubmissionNotice${index} > option:nth-child(${actionTypeIndex + 1})`));
actionTypeSelection.click();
}
async function autoskipSegment(driver: WebDriver, startTime: number, endTime: number): Promise<void> { async function autoskipSegment(driver: WebDriver, startTime: number, endTime: number): Promise<void> {
const video = await driver.findElement(By.css("video")); const video = await driver.findElement(By.css("video"));
@ -113,7 +126,21 @@ async function autoskipSegment(driver: WebDriver, startTime: number, endTime: nu
await driver.executeScript("document.querySelector('video').play()"); await driver.executeScript("document.querySelector('video').play()");
await driver.sleep(1300); await driver.sleep(1300);
expect(parseFloat(await video.getAttribute("currentTime"))).toBeGreaterThan(endTime); expect(parseFloat(await video.getAttribute("currentTime"))).toBeGreaterThan(endTime);
await driver.executeScript("document.querySelector('video').pause()"); await driver.executeScript("document.querySelector('video').pause()");
}
async function muteSkipSegment(driver: WebDriver, startTime: number, endTime: number): Promise<void> {
const duration = endTime - startTime;
const video = await driver.findElement(By.css("video"));
await driver.executeScript("document.querySelector('video').currentTime = " + (startTime - 0.5));
await driver.executeScript("document.querySelector('video').play()");
await driver.sleep(1300);
expect(await video.getAttribute("muted")).toEqual("true");
await driver.sleep(duration * 1000 + 300);
expect(await video.getAttribute("muted")).toBeNull(); // Default is null for some reason
await driver.executeScript("document.querySelector('video').pause()");
} }

View file

@ -8,6 +8,11 @@
"noEmitOnError": false, "noEmitOnError": false,
"typeRoots": [ "node_modules/@types" ], "typeRoots": [ "node_modules/@types" ],
"resolveJsonModule": true, "resolveJsonModule": true,
"jsx": "react" "jsx": "react",
"lib": [
"es2019",
"dom",
"dom.iterable"
]
} }
} }