Add advanced skip options

This commit is contained in:
Ajay
2025-06-06 22:03:07 -04:00
parent cf2d39ae3f
commit 09bd27a59b
12 changed files with 450 additions and 37 deletions

View File

@@ -732,3 +732,16 @@ svg {
.dearrow-link:hover .close-button { .dearrow-link:hover .close-button {
opacity: 1; opacity: 1;
} }
.invalid-advanced-config {
color: red;
}
.advanced-skip-options-menu {
margin-top: 10px;
}
.advanced-config-help-message {
margin-bottom: 10px;
transition: none;
}

View File

@@ -140,6 +140,8 @@
<div class="small-description">__MSG_whatManualSkipOnFullVideo__</div> <div class="small-description">__MSG_whatManualSkipOnFullVideo__</div>
</div> </div>
<div data-type="react-AdvancedSkipOptionsComponent"></div>
<div data-type="toggle" data-sync="forceChannelCheck"> <div data-type="toggle" data-sync="forceChannelCheck">
<div class="switch-container"> <div class="switch-container">
<label class="switch"> <label class="switch">

View File

@@ -0,0 +1,269 @@
import * as React from "react";
import * as CompileConfig from "../../../config.json";
import Config, { AdvancedSkipRuleSet, SkipRuleAttribute, SkipRuleOperator } from "../../config";
import { CategorySkipOption } from "../../types";
let configSaveTimeout: NodeJS.Timeout | null = null;
export function AdvancedSkipOptionsComponent() {
const [optionsOpen, setOptionsOpen] = React.useState(false);
const [config, setConfig] = React.useState(configToText(Config.local.skipRules));
const [configValid, setConfigValid] = React.useState(true);
return (
<div>
<div className="option-button" onClick={() => {
setOptionsOpen(!optionsOpen);
}}>
{chrome.i18n.getMessage("openAdvancedSkipOptions")}
</div>
{
optionsOpen &&
<div className="advanced-skip-options-menu">
<div className={"advanced-config-help-message"}>
<a target="_blank"
rel="noopener noreferrer"
href="https://wiki.sponsor.ajay.app/w/Advanced_Skip_Options">
{chrome.i18n.getMessage("advancedSkipSettingsHelp")}
</a>
<span className={configValid ? "hidden" : "invalid-advanced-config"}>
{" - "}
{chrome.i18n.getMessage("advancedSkipNotSaved")}
</span>
</div>
<textarea className={"option-text-box " + (configValid ? "" : "invalid-advanced-config")}
rows={10}
style={{ width: "80%" }}
value={config}
spellCheck={false}
onChange={(e) => {
setConfig(e.target.value);
const compiled = compileConfig(e.target.value);
setConfigValid(!!compiled && !(e.target.value.length > 0 && compiled.length === 0));
if (compiled) {
if (configSaveTimeout) {
clearTimeout(configSaveTimeout);
}
configSaveTimeout = setTimeout(() => {
Config.local.skipRules = compiled;
}, 200);
}
}}
/>
</div>
}
</div>
);
}
function compileConfig(config: string): AdvancedSkipRuleSet[] | null {
const ruleSets: AdvancedSkipRuleSet[] = [];
let ruleSet: AdvancedSkipRuleSet = {
rules: [],
skipOption: null,
comment: ""
};
for (const line of config.split("\n")) {
if (line.trim().length === 0) {
// Skip empty lines
continue;
}
const comment = line.match(/^\s*\/\/(.+)$/);
if (comment) {
if (ruleSet.rules.length > 0) {
// Rule has already been created, add it to list if valid
if (ruleSet.skipOption !== null && ruleSet.rules.length > 0) {
ruleSets.push(ruleSet);
ruleSet = {
rules: [],
skipOption: null,
comment: ""
};
} else {
return null;
}
}
if (ruleSet.comment.length > 0) {
ruleSet.comment += "; ";
}
ruleSet.comment += comment[1].trim();
// Skip comment lines
continue;
} else if (line.startsWith("if ")) {
if (ruleSet.rules.length > 0) {
// Rule has already been created, add it to list if valid
if (ruleSet.skipOption !== null && ruleSet.rules.length > 0) {
ruleSets.push(ruleSet);
ruleSet = {
rules: [],
skipOption: null,
comment: ""
};
} else {
return null;
}
}
const ruleTexts = [...line.matchAll(/\S+ \S+ (?:"[^"\\]*(?:\\.[^"\\]*)*"|\d+)(?= and |$)/g)];
for (const ruleText of ruleTexts) {
if (!ruleText[0]) return null;
const ruleParts = ruleText[0].match(/(\S+) (\S+) ("[^"\\]*(?:\\.[^"\\]*)*"|\d+)/);
if (ruleParts.length !== 4) {
return null; // Invalid rule format
}
const attribute = getSkipRuleAttribute(ruleParts[1]);
const operator = getSkipRuleOperator(ruleParts[2]);
const value = getSkipRuleValue(ruleParts[3]);
if (attribute === null || operator === null || value === null) {
return null; // Invalid attribute or operator
}
if (attribute === SkipRuleAttribute.Category
&& operator === SkipRuleOperator.Equal
&& !CompileConfig.categoryList.includes(value as string)) {
return null; // Invalid category value
} else if (attribute === SkipRuleAttribute.Source
&& operator === SkipRuleOperator.Equal
&& !["local", "youtube", "server"].includes(value as string)) {
return null; // Invalid category value
}
ruleSet.rules.push({
attribute,
operator,
value
});
}
// Make sure all rules were parsed
if (ruleTexts.length === 0 || !line.endsWith(ruleTexts[ruleTexts.length - 1][0])) {
return null;
}
} else {
// Only continue if a rule has been defined
if (ruleSet.rules.length === 0) {
return null; // No rules defined yet
}
switch (line.trim().toLowerCase()) {
case "disabled":
ruleSet.skipOption = CategorySkipOption.Disabled;
break;
case "show overlay":
ruleSet.skipOption = CategorySkipOption.ShowOverlay;
break;
case "manual skip":
ruleSet.skipOption = CategorySkipOption.ManualSkip;
break;
case "auto skip":
ruleSet.skipOption = CategorySkipOption.AutoSkip;
break;
default:
return null; // Invalid skip option
}
}
}
if (ruleSet.rules.length > 0 && ruleSet.skipOption !== null) {
ruleSets.push(ruleSet);
} else if (ruleSet.rules.length > 0 || ruleSet.skipOption !== null) {
// Incomplete rule set
return null;
}
return ruleSets;
}
function getSkipRuleAttribute(attribute: string): SkipRuleAttribute | null {
if (attribute && Object.values(SkipRuleAttribute).includes(attribute as SkipRuleAttribute)) {
return attribute as SkipRuleAttribute;
}
return null;
}
function getSkipRuleOperator(operator: string): SkipRuleOperator | null {
if (operator && Object.values(SkipRuleOperator).includes(operator as SkipRuleOperator)) {
return operator as SkipRuleOperator;
}
return null;
}
function getSkipRuleValue(value: string): string | number | null {
if (!value) return null;
if (value.startsWith('"')) {
try {
return JSON.parse(value);
} catch (e) {
return null; // Invalid JSON string
}
} else {
const numValue = Number(value);
if (!isNaN(numValue)) {
return numValue;
}
return null;
}
}
function configToText(config: AdvancedSkipRuleSet[]): string {
let result = "";
for (const ruleSet of config) {
if (ruleSet.comment) {
result += "// " + ruleSet.comment + "\n";
}
result += "if ";
let firstRule = true;
for (const rule of ruleSet.rules) {
if (!firstRule) {
result += " and ";
}
result += `${rule.attribute} ${rule.operator} ${JSON.stringify(rule.value)}`;
firstRule = false;
}
switch (ruleSet.skipOption) {
case CategorySkipOption.Disabled:
result += "\nDisabled";
break;
case CategorySkipOption.ShowOverlay:
result += "\nShow Overlay";
break;
case CategorySkipOption.ManualSkip:
result += "\nManual Skip";
break;
case CategorySkipOption.AutoSkip:
result += "\nAuto Skip";
break;
default:
return null; // Invalid skip option
}
result += "\n\n";
}
return result.trim();
}

View File

@@ -8,6 +8,38 @@ export interface Permission {
canSubmit: boolean; canSubmit: boolean;
} }
export enum SkipRuleAttribute {
StartTime = "startTime",
EndTime = "endTime",
Duration = "duration",
Category = "category",
Description = "description",
Source = "source"
}
export enum SkipRuleOperator {
Less = "<",
LessOrEqual = "<=",
Greater = ">",
GreaterOrEqual = ">=",
Equal = "==",
NotEqual = "!=",
Contains = "*=",
Regex = "~="
}
export interface AdvancedSkipRule {
attribute: SkipRuleAttribute;
operator: SkipRuleOperator;
value: string | number;
}
export interface AdvancedSkipRuleSet {
rules: AdvancedSkipRule[];
skipOption: CategorySkipOption;
comment: string;
}
interface SBConfig { interface SBConfig {
userID: string; userID: string;
isVip: boolean; isVip: boolean;
@@ -149,6 +181,8 @@ interface SBStorage {
/* Contains unsubmitted segments that the user has created. */ /* Contains unsubmitted segments that the user has created. */
unsubmittedSegments: Record<string, SponsorTime[]>; unsubmittedSegments: Record<string, SponsorTime[]>;
skipRules: AdvancedSkipRuleSet[];
} }
class ConfigClass extends ProtoConfig<SBConfig, SBStorage> { class ConfigClass extends ProtoConfig<SBConfig, SBStorage> {
@@ -168,6 +202,15 @@ class ConfigClass extends ProtoConfig<SBConfig, SBStorage> {
} }
function migrateOldSyncFormats(config: SBConfig) { function migrateOldSyncFormats(config: SBConfig) {
if (!config["changeChapterColor"]) {
config.barTypes["chapter"].color = "#ffd983";
config["changeChapterColor"] = true;
chrome.storage.sync.set({
"changeChapterColor": true,
"barTypes": config.barTypes
});
}
if (config["showZoomToFillError"]) { if (config["showZoomToFillError"]) {
chrome.storage.sync.remove("showZoomToFillError"); chrome.storage.sync.remove("showZoomToFillError");
} }
@@ -474,7 +517,7 @@ const syncDefaults = {
opacity: "0.7" opacity: "0.7"
}, },
"chapter": { "chapter": {
color: "#fff", color: "#ffd983",
opacity: "0" opacity: "0"
}, },
} }
@@ -485,7 +528,8 @@ const localDefaults = {
navigationApiAvailable: null, navigationApiAvailable: null,
alreadyInstalled: false, alreadyInstalled: false,
unsubmittedSegments: {} unsubmittedSegments: {},
skipRules: []
}; };
const Config = new ConfigClass(syncDefaults, localDefaults, migrateOldSyncFormats); const Config = new ConfigClass(syncDefaults, localDefaults, migrateOldSyncFormats);

View File

@@ -51,7 +51,7 @@ import { asyncRequestToServer } from "./utils/requests";
import { isMobileControlsOpen } from "./utils/mobileUtils"; import { isMobileControlsOpen } from "./utils/mobileUtils";
import { defaultPreviewTime } from "./utils/constants"; import { defaultPreviewTime } from "./utils/constants";
import { onVideoPage } from "../maze-utils/src/pageInfo"; import { onVideoPage } from "../maze-utils/src/pageInfo";
import { getSegmentsForVideo } from "./utils/segmentData"; import { getCategoryDefaultSelection, getCategorySelection, getSegmentsForVideo } from "./utils/segmentData";
cleanPage(); cleanPage();
@@ -299,7 +299,6 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
break; break;
} }
loopedChapter = {...utils.getSponsorTimeFromUUID(sponsorTimes, request.UUID)}; loopedChapter = {...utils.getSponsorTimeFromUUID(sponsorTimes, request.UUID)};
loopedChapter.actionType = ActionType.Skip;
loopedChapter.segment = [loopedChapter.segment[1], loopedChapter.segment[0]]; loopedChapter.segment = [loopedChapter.segment[1], loopedChapter.segment[0]];
break; break;
case "importSegments": { case "importSegments": {
@@ -312,7 +311,7 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
&& s.description === segment.description)) { && s.description === segment.description)) {
const hasChaptersPermission = (Config.config.showCategoryWithoutPermission const hasChaptersPermission = (Config.config.showCategoryWithoutPermission
|| Config.config.permissions["chapter"]); || Config.config.permissions["chapter"]);
if (segment.category === "chapter" && (!utils.getCategorySelection("chapter") || !hasChaptersPermission)) { if (segment.category === "chapter" && (!getCategoryDefaultSelection("chapter") || !hasChaptersPermission)) {
segment.category = "chooseACategory" as Category; segment.category = "chooseACategory" as Category;
segment.actionType = ActionType.Skip; segment.actionType = ActionType.Skip;
segment.description = ""; segment.description = "";
@@ -734,7 +733,7 @@ async function startSponsorSchedule(includeIntersectingSegments = false, current
} }
} }
if (utils.getCategorySelection(currentSkip.category)?.option === CategorySkipOption.ManualSkip if (getCategorySelection(currentSkip)?.option === CategorySkipOption.ManualSkip
|| currentSkip.actionType === ActionType.Mute) { || currentSkip.actionType === ActionType.Mute) {
forcedSkipTime = skipTime[0] + 0.001; forcedSkipTime = skipTime[0] + 0.001;
} else { } else {
@@ -1355,7 +1354,7 @@ function startSkipScheduleCheckingForStartSponsors() {
&& time.actionType === ActionType.Poi && time.hidden === SponsorHideType.Visible) && time.actionType === ActionType.Poi && time.hidden === SponsorHideType.Visible)
.sort((a, b) => b.segment[0] - a.segment[0]); .sort((a, b) => b.segment[0] - a.segment[0]);
for (const time of poiSegments) { for (const time of poiSegments) {
const skipOption = utils.getCategorySelection(time.category)?.option; const skipOption = getCategorySelection(time)?.option;
if (skipOption !== CategorySkipOption.ShowOverlay) { if (skipOption !== CategorySkipOption.ShowOverlay) {
skipToTime({ skipToTime({
v: getVideo(), v: getVideo(),
@@ -1504,12 +1503,12 @@ function getNextSkipIndex(currentTime: number, includeIntersectingSegments: bool
{array: ScheduledTime[]; index: number; endIndex: number; extraIndexes: number[]; openNotice: boolean} { {array: ScheduledTime[]; index: number; endIndex: number; extraIndexes: number[]; openNotice: boolean} {
const autoSkipSorter = (segment: ScheduledTime) => { const autoSkipSorter = (segment: ScheduledTime) => {
const skipOption = utils.getCategorySelection(segment.category)?.option; const skipOption = getCategorySelection(segment)?.option;
if (segment.hidden !== SponsorHideType.Visible) { if (segment.hidden !== SponsorHideType.Visible) {
// Hidden segments sometimes end up here if another segment is at the same time, use them last // Hidden segments sometimes end up here if another segment is at the same time, use them last
return 3; return 3;
} else if ((skipOption === CategorySkipOption.AutoSkip || shouldAutoSkip(segment)) } else if ((skipOption === CategorySkipOption.AutoSkip || shouldAutoSkip(segment))
&& segment.actionType === ActionType.Skip) { && (segment.actionType === ActionType.Skip || segment.actionType === ActionType.Chapter)) {
return 0; return 0;
} else if (skipOption !== CategorySkipOption.ShowOverlay) { } else if (skipOption !== CategorySkipOption.ShowOverlay) {
return 1; return 1;
@@ -1728,6 +1727,7 @@ function skipToTime({v, skipTime, skippingSegments, openNotice, forceAutoSkip, u
&& getCurrentTime() !== skipTime[1]) { && getCurrentTime() !== skipTime[1]) {
switch(skippingSegments[0].actionType) { switch(skippingSegments[0].actionType) {
case ActionType.Poi: case ActionType.Poi:
case ActionType.Chapter:
case ActionType.Skip: { case ActionType.Skip: {
// Fix for looped videos not working when skipping to the end #426 // Fix for looped videos not working when skipping to the end #426
// for some reason you also can't skip to 1 second before the end // for some reason you also can't skip to 1 second before the end
@@ -1850,7 +1850,7 @@ function unskipSponsorTime(segment: SponsorTime, unskipTime: number = null, forc
videoMuted = false; videoMuted = false;
} }
if (forceSeek || segment.actionType === ActionType.Skip || voteNotice) { if (forceSeek || segment.actionType === ActionType.Skip || segment.actionType === ActionType.Chapter || voteNotice) {
//add a tiny bit of time to make sure it is not skipped again //add a tiny bit of time to make sure it is not skipped again
setCurrentTime(unskipTime ?? segment.segment[0] + 0.001); setCurrentTime(unskipTime ?? segment.segment[0] + 0.001);
} }
@@ -1921,7 +1921,7 @@ function shouldAutoSkip(segment: SponsorTime): boolean {
} }
return (!Config.config.manualSkipOnFullVideo || !sponsorTimes?.some((s) => s.category === segment.category && s.actionType === ActionType.Full)) return (!Config.config.manualSkipOnFullVideo || !sponsorTimes?.some((s) => s.category === segment.category && s.actionType === ActionType.Full))
&& (utils.getCategorySelection(segment.category)?.option === CategorySkipOption.AutoSkip || && (getCategorySelection(segment)?.option === CategorySkipOption.AutoSkip ||
(Config.config.autoSkipOnMusicVideos && canSkipNonMusic && sponsorTimes?.some((s) => s.category === "music_offtopic") (Config.config.autoSkipOnMusicVideos && canSkipNonMusic && sponsorTimes?.some((s) => s.category === "music_offtopic")
&& segment.actionType === ActionType.Skip) && segment.actionType === ActionType.Skip)
|| sponsorTimesSubmitting.some((s) => s.segment === segment.segment)) || sponsorTimesSubmitting.some((s) => s.segment === segment.segment))
@@ -1930,15 +1930,14 @@ function shouldAutoSkip(segment: SponsorTime): boolean {
function shouldSkip(segment: SponsorTime): boolean { function shouldSkip(segment: SponsorTime): boolean {
return (segment.actionType !== ActionType.Full return (segment.actionType !== ActionType.Full
&& segment.source !== SponsorSourceType.YouTube && getCategorySelection(segment)?.option > CategorySkipOption.ShowOverlay)
&& 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")
&& segment.actionType === ActionType.Skip) && segment.actionType === ActionType.Skip)
|| isLoopedChapter(segment); || isLoopedChapter(segment);
} }
function isLoopedChapter(segment: SponsorTime) :boolean{ function isLoopedChapter(segment: SponsorTime): boolean{
return !!segment && !!loopedChapter && segment.actionType === ActionType.Skip && segment.segment[1] != undefined return !!segment && !!loopedChapter && segment.segment[1] != undefined
&& segment.segment[0] === loopedChapter.segment[0] && segment.segment[1] === loopedChapter.segment[1]; && segment.segment[0] === loopedChapter.segment[0] && segment.segment[1] === loopedChapter.segment[1];
} }

View File

@@ -18,6 +18,7 @@ import { getHash } from "../maze-utils/src/hash";
import { isFirefoxOrSafari } from "../maze-utils/src"; import { isFirefoxOrSafari } from "../maze-utils/src";
import { isDeArrowInstalled } from "./utils/crossExtension"; import { isDeArrowInstalled } from "./utils/crossExtension";
import { asyncRequestToServer } from "./utils/requests"; import { asyncRequestToServer } from "./utils/requests";
import AdvancedSkipOptions from "./render/AdvancedSkipOptions";
const utils = new Utils(); const utils = new Utils();
let embed = false; let embed = false;
@@ -350,6 +351,9 @@ async function init() {
case "react-CategoryChooserComponent": case "react-CategoryChooserComponent":
categoryChoosers.push(new CategoryChooser(optionsElements[i])); categoryChoosers.push(new CategoryChooser(optionsElements[i]));
break; break;
case "react-AdvancedSkipOptionsComponent":
new AdvancedSkipOptions(optionsElements[i]);
break;
case "react-UnsubmittedVideosComponent": case "react-UnsubmittedVideosComponent":
unsubmittedVideos.push(new UnsubmittedVideos(optionsElements[i])); unsubmittedVideos.push(new UnsubmittedVideos(optionsElements[i]));
break; break;

View File

@@ -0,0 +1,15 @@
import * as React from "react";
import { createRoot } from 'react-dom/client';
import { AdvancedSkipOptionsComponent } from "../components/options/AdvancedSkipOptionsComponent";
class AdvancedSkipOptions {
constructor(element: Element) {
const root = createRoot(element);
root.render(
<AdvancedSkipOptionsComponent />
);
}
}
export default AdvancedSkipOptions;

View File

@@ -1,5 +1,5 @@
import Config, { VideoDownvotes } from "./config"; import Config, { VideoDownvotes } from "./config";
import { CategorySelection, SponsorTime, BackgroundScriptContainer, Registration, VideoID, SponsorHideType, CategorySkipOption } from "./types"; import { SponsorTime, BackgroundScriptContainer, Registration, VideoID, SponsorHideType } from "./types";
import { getHash, HashedValue } from "../maze-utils/src/hash"; import { getHash, HashedValue } from "../maze-utils/src/hash";
import { waitFor } from "../maze-utils/src"; import { waitFor } from "../maze-utils/src";
@@ -211,15 +211,6 @@ export default class Utils {
return sponsorTimes[this.getSponsorIndexFromUUID(sponsorTimes, UUID)]; return sponsorTimes[this.getSponsorIndexFromUUID(sponsorTimes, UUID)];
} }
getCategorySelection(category: string): CategorySelection {
for (const selection of Config.config.categorySelections) {
if (selection.name === category) {
return selection;
}
}
return { name: category, option: CategorySkipOption.Disabled} as CategorySelection;
}
/** /**
* @returns {String[]} Domains in regex form * @returns {String[]} Domains in regex form
*/ */
@@ -321,7 +312,6 @@ export default class Utils {
allDownvotes[hashedVideoID] = currentVideoData; allDownvotes[hashedVideoID] = currentVideoData;
} }
console.log(allDownvotes)
const entries = Object.entries(allDownvotes); const entries = Object.entries(allDownvotes);
if (entries.length > 10000) { if (entries.length > 10000) {

View File

@@ -6,6 +6,7 @@ export function getSkippingText(segments: SponsorTime[], autoSkip: boolean): str
if (autoSkip) { if (autoSkip) {
let messageId = ""; let messageId = "";
switch (segments[0].actionType) { switch (segments[0].actionType) {
case ActionType.Chapter:
case ActionType.Skip: case ActionType.Skip:
messageId = "skipped"; messageId = "skipped";
break; break;
@@ -21,6 +22,7 @@ export function getSkippingText(segments: SponsorTime[], autoSkip: boolean): str
} else { } else {
let messageId = ""; let messageId = "";
switch (segments[0].actionType) { switch (segments[0].actionType) {
case ActionType.Chapter:
case ActionType.Skip: case ActionType.Skip:
messageId = "skip_category"; messageId = "skip_category";
break; break;

View File

@@ -1,11 +1,12 @@
import { DataCache } from "../../maze-utils/src/cache"; import { DataCache } from "../../maze-utils/src/cache";
import { getHash, HashedValue } from "../../maze-utils/src/hash"; import { getHash, HashedValue } from "../../maze-utils/src/hash";
import Config from "../config"; import Config, { AdvancedSkipRule, SkipRuleAttribute, SkipRuleOperator } from "../config";
import * as CompileConfig from "../../config.json"; import * as CompileConfig from "../../config.json";
import { ActionType, ActionTypes, SponsorSourceType, SponsorTime, VideoID } from "../types"; import { ActionType, ActionTypes, CategorySelection, CategorySkipOption, SponsorSourceType, SponsorTime, VideoID } from "../types";
import { getHashParams } from "./pageUtils"; import { getHashParams } from "./pageUtils";
import { asyncRequestToServer } from "./requests"; import { asyncRequestToServer } from "./requests";
import { extensionUserAgent } from "../../maze-utils/src"; import { extensionUserAgent } from "../../maze-utils/src";
import { VideoLabelsCacheData } from "./videoLabels";
const segmentDataCache = new DataCache<VideoID, SegmentResponse>(() => { const segmentDataCache = new DataCache<VideoID, SegmentResponse>(() => {
return { return {
@@ -44,8 +45,6 @@ export async function getSegmentsForVideo(videoID: VideoID, ignoreCache: boolean
} }
async function fetchSegmentsForVideo(videoID: VideoID): Promise<SegmentResponse> { async function fetchSegmentsForVideo(videoID: VideoID): Promise<SegmentResponse> {
const categories: string[] = Config.config.categorySelections.map((category) => category.name);
const extraRequestData: Record<string, unknown> = {}; const extraRequestData: Record<string, unknown> = {};
const hashParams = getHashParams(); const hashParams = getHashParams();
if (hashParams.requiredSegment) extraRequestData.requiredSegment = hashParams.requiredSegment; if (hashParams.requiredSegment) extraRequestData.requiredSegment = hashParams.requiredSegment;
@@ -67,7 +66,8 @@ async function fetchSegmentsForVideo(videoID: VideoID): Promise<SegmentResponse>
const receivedSegments: SponsorTime[] = JSON.parse(response.responseText) const receivedSegments: SponsorTime[] = JSON.parse(response.responseText)
?.filter((video) => video.videoID === videoID) ?.filter((video) => video.videoID === videoID)
?.map((video) => video.segments)?.[0] ?.map((video) => video.segments)?.[0]
?.filter((segment) => enabledActionTypes.includes(segment.actionType) && categories.includes(segment.category)) ?.filter((segment) => enabledActionTypes.includes(segment.actionType)
&& getCategorySelection(segment).option !== CategorySkipOption.Disabled)
?.map((segment) => ({ ?.map((segment) => ({
...segment, ...segment,
source: SponsorSourceType.Server source: SponsorSourceType.Server
@@ -105,3 +105,80 @@ function getEnabledActionTypes(forceFullVideo = false): ActionType[] {
return actionTypes; return actionTypes;
} }
export function getCategorySelection(segment: SponsorTime | VideoLabelsCacheData): CategorySelection {
for (const ruleSet of Config.local.skipRules) {
if (ruleSet.rules.every((rule) => isSkipRulePassing(segment, rule))) {
return { name: segment.category, option: ruleSet.skipOption } as CategorySelection;
}
}
for (const selection of Config.config.categorySelections) {
if (selection.name === segment.category) {
return selection;
}
}
return { name: segment.category, option: CategorySkipOption.Disabled} as CategorySelection;
}
function getSkipRuleValue(segment: SponsorTime | VideoLabelsCacheData, rule: AdvancedSkipRule): string | number | undefined {
switch (rule.attribute) {
case SkipRuleAttribute.StartTime:
return (segment as SponsorTime).segment?.[0];
case SkipRuleAttribute.EndTime:
return (segment as SponsorTime).segment?.[1];
case SkipRuleAttribute.Duration:
return (segment as SponsorTime).segment?.[1] - (segment as SponsorTime).segment?.[0];
case SkipRuleAttribute.Category:
return segment.category;
case SkipRuleAttribute.Description:
return (segment as SponsorTime).description || "";
case SkipRuleAttribute.Source:
switch ((segment as SponsorTime).source) {
case SponsorSourceType.Local:
return "local";
case SponsorSourceType.YouTube:
return "youtube";
case SponsorSourceType.Server:
return "server";
}
break;
default:
return undefined;
}
}
function isSkipRulePassing(segment: SponsorTime | VideoLabelsCacheData, rule: AdvancedSkipRule): boolean {
const value = getSkipRuleValue(segment, rule);
switch (rule.operator) {
case SkipRuleOperator.Less:
return typeof value === "number" && value < (rule.value as number);
case SkipRuleOperator.LessOrEqual:
return typeof value === "number" && value <= (rule.value as number);
case SkipRuleOperator.Greater:
return typeof value === "number" && value > (rule.value as number);
case SkipRuleOperator.GreaterOrEqual:
return typeof value === "number" && value >= (rule.value as number);
case SkipRuleOperator.Equal:
return value === rule.value;
case SkipRuleOperator.NotEqual:
return value !== rule.value;
case SkipRuleOperator.Contains:
return String(value).includes(String(rule.value));
case SkipRuleOperator.Regex:
return new RegExp(rule.value as string).test(String(value));
default:
return false;
}
}
export function getCategoryDefaultSelection(category: string): CategorySelection {
for (const selection of Config.config.categorySelections) {
if (selection.name === category) {
return selection;
}
}
return { name: category, option: CategorySkipOption.Disabled} as CategorySelection;
}

View File

@@ -1,12 +1,10 @@
import { Category, CategorySkipOption, VideoID } from "../types"; import { Category, CategorySkipOption, VideoID } from "../types";
import { getHash } from "../../maze-utils/src/hash"; import { getHash } from "../../maze-utils/src/hash";
import Utils from "../utils";
import { logWarn } from "./logger"; import { logWarn } from "./logger";
import { asyncRequestToServer } from "./requests"; import { asyncRequestToServer } from "./requests";
import { getCategorySelection } from "./segmentData";
const utils = new Utils(); export interface VideoLabelsCacheData {
interface VideoLabelsCacheData {
category: Category; category: Category;
hasStartSegment: boolean; hasStartSegment: boolean;
} }
@@ -68,7 +66,7 @@ export async function getVideoLabel(videoID: VideoID): Promise<Category | null>
if (result) { if (result) {
const category = result.videos[videoID]?.category; const category = result.videos[videoID]?.category;
if (category && utils.getCategorySelection(category).option !== CategorySkipOption.Disabled) { if (category && getCategorySelection(result.videos[videoID]).option !== CategorySkipOption.Disabled) {
return category; return category;
} else { } else {
return null; return null;