From 3ff1a6b8e47a43806b665129d2e60cd330ecd924 Mon Sep 17 00:00:00 2001 From: mini-bomba <55105495+mini-bomba@users.noreply.github.com> Date: Mon, 10 Oct 2022 22:54:25 +0200 Subject: [PATCH 1/2] Add checks for overlap with existing segments to category locked errors This will notify the user if they attempted to submit a segment that already exists. --- src/routes/postSkipSegments.ts | 74 ++++++++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 3 deletions(-) diff --git a/src/routes/postSkipSegments.ts b/src/routes/postSkipSegments.ts index 97cd3c72..c90f94bd 100644 --- a/src/routes/postSkipSegments.ts +++ b/src/routes/postSkipSegments.ts @@ -9,7 +9,7 @@ import { getIP } from "../utils/getIP"; import { getFormattedTime } from "../utils/getFormattedTime"; import { dispatchEvent } from "../utils/webhookUtils"; import { Request, Response } from "express"; -import { ActionType, Category, IncomingSegment, IPAddress, SegmentUUID, Service, VideoDuration, VideoID } from "../types/segments.model"; +import { ActionType, Category, DBSegment, IncomingSegment, IPAddress, SegmentUUID, Service, VideoDuration, VideoID } from "../types/segments.model"; import { deleteLockCategories } from "./deleteLockCategories"; import { QueryCacher } from "../utils/queryCacher"; import { getReputation } from "../utils/reputation"; @@ -238,6 +238,72 @@ async function checkInvalidFields(videoID: VideoID, userID: UserID, hashedUserID return CHECK_PASS; } +function fetchOverlappingSegmentCanditates(videoID: VideoID, service: Service, segment: IncomingSegment): Promise { + switch (segment.actionType) { + case ActionType.Poi: + case ActionType.Full: // There can only be one of those per video, return only the most likely one to be shown + return db.prepare( + "all", + `SELECT "startTime", "endTime", "votes", "locked", "category", "actionType" FROM "sponsorTimes" + WHERE "videoID" = ? AND "service" = ? AND "actionType" = ? AND "votes" > -2 AND "hidden" = 0 AND "shadowHidden" = 0 + ORDER BY "locked" DESC, "votes" DESC, "reputation" DESC + LIMIT 1`, + [videoID, service, segment.actionType], + { useReplica: true } + ); + case ActionType.Skip: + case ActionType.Mute: + return db.prepare( + "all", + `SELECT "startTime", "endTime", "votes", "locked", "category", "actionType" FROM "sponsorTimes" + WHERE "videoID" = ? AND "service" = ? AND ("actionType" = 'skip' OR "actionType" = 'mute') AND "votes" > -2 AND "hidden" = 0 AND "shadowHidden" = 0 + ORDER BY "locked" DESC, "votes" DESC, "reputation" DESC`, + [videoID, service], + { useReplica: true } + ); + default: + return db.prepare( + "all", + `SELECT "startTime", "endTime", "votes", "locked", "category", "actionType", "description" FROM "sponsorTimes" + WHERE "videoID" = ? AND "service" = ? AND "actionType" = ? AND "votes" > -2 AND "hidden" = 0 AND "shadowHidden" = 0 + ORDER BY "locked" DESC, "votes" DESC, "reputation" DESC`, + [videoID, service, segment.actionType], + { useReplica: true } + ); + } +} + +async function checkSegmentOverlap(videoID: VideoID, service: Service, incomingSegment: IncomingSegment): Promise { + const candidates = await fetchOverlappingSegmentCanditates(videoID, service, incomingSegment); + + if (candidates.length === 0) return null; // Can't overlap if there are no segments + + if (incomingSegment.actionType === ActionType.Poi || incomingSegment.actionType === ActionType.Full) { + const bestSegment = candidates[0]; // fetchOverlappingSegmentCanditates returns only one segment for these + return `\nA ${bestSegment.actionType} ${bestSegment.category} segment ${incomingSegment.actionType === ActionType.Poi ? `at ${getFormattedTime(bestSegment.startTime)} ` : ""}has already been submitted.\n` + + `If this is what you tried to submit, please make sure you have the ${bestSegment.category} category enabled and try refreshing the segment list.\n`; + } + const startTime = parseFloat(incomingSegment.segment[0]); + const endTime = parseFloat(incomingSegment.segment[1]); + const bestSegment = candidates.find((segment) => { + const overlap = Math.min(segment.endTime, endTime) - Math.max(segment.startTime, startTime); + const overallDuration = Math.max(segment.endTime, endTime) - Math.min(segment.startTime, startTime); + const overlapPercent = overlap / overallDuration; + return (overlapPercent >= 0.1 && segment.actionType === incomingSegment.actionType && segment.actionType !== ActionType.Chapter) + || (overlapPercent >= 0.6 && segment.actionType !== incomingSegment.actionType) + || (overlapPercent >= 0.8 && segment.actionType === ActionType.Chapter && incomingSegment.actionType === ActionType.Chapter); + }); + + if (!bestSegment) return null; // Overlap not found + + if (bestSegment.actionType === ActionType.Chapter) + return `\nA "${bestSegment.description}" chapter at ${getFormattedTime(bestSegment.startTime)}-${getFormattedTime(bestSegment.endTime)} has already been submitted.\n` + + `If this is what you tried to submit, please make sure you have the ${bestSegment.category} category enabled and try refreshing the segment list.\n`; + + return `\nA ${bestSegment.actionType} ${bestSegment.category} segment at ${getFormattedTime(bestSegment.startTime)}-${getFormattedTime(bestSegment.endTime)} has already been submitted.\n` + + `If this is what you tried to submit, please make sure you have the ${bestSegment.category} category enabled and try refreshing the segment list.\n`; +} + async function checkEachSegmentValid(rawIP: IPAddress, paramUserID: UserID, userID: HashedUserID, videoID: VideoID, segments: IncomingSegment[], service: Service, isVIP: boolean, lockedCategoryList: Array): Promise { @@ -261,6 +327,8 @@ async function checkEachSegmentValid(rawIP: IPAddress, paramUserID: UserID, user userID }); + const overlapMessage = await checkSegmentOverlap(videoID, service, segments[i]); + Logger.warn(`Caught a submission for a locked category. userID: '${userID}', videoID: '${videoID}', category: '${segments[i].category}', times: ${segments[i].segment}`); return { pass: false, @@ -269,8 +337,8 @@ async function checkEachSegmentValid(rawIP: IPAddress, paramUserID: UserID, user `Users have voted that new segments aren't needed for the following category: ` + `'${segments[i].category}'\n` + `${lockedCategoryList[lockIndex].reason?.length !== 0 ? `\nReason: '${lockedCategoryList[lockIndex].reason}'` : ""}\n` + - `${(segments[i].category === "sponsor" ? "\nMaybe the segment you are submitting is a different category that you have not enabled and is not a sponsor. " + - "Categories that aren't sponsor, such as self-promotion can be enabled in the options.\n" : "")}` + + (overlapMessage ?? (segments[i].category === "sponsor" ? "\nMaybe the segment you are submitting is a different category that you have not enabled and is not a sponsor. " + + "Categories that aren't sponsor, such as self-promotion can be enabled in the options.\n" : "")) + `\nIf you believe this is incorrect, please contact someone on chat.sponsor.ajay.app, discord.gg/SponsorBlock or matrix.to/#/#sponsor:ajay.app` }; } From 2233cfe6ec77f862c56e84ba29e161a218074300 Mon Sep 17 00:00:00 2001 From: Ajay Date: Tue, 11 Oct 2022 00:26:41 -0400 Subject: [PATCH 2/2] Formatting and code de-duplication --- src/routes/getSkipSegments.ts | 10 ++------ src/routes/postSkipSegments.ts | 43 +++++++++++++++++----------------- src/utils/segments.ts | 17 ++++++++++++++ 3 files changed, 41 insertions(+), 29 deletions(-) create mode 100644 src/utils/segments.ts diff --git a/src/routes/getSkipSegments.ts b/src/routes/getSkipSegments.ts index 7da485e6..44d31d50 100644 --- a/src/routes/getSkipSegments.ts +++ b/src/routes/getSkipSegments.ts @@ -12,6 +12,7 @@ import { QueryCacher } from "../utils/queryCacher"; import { getReputation } from "../utils/reputation"; import { getService } from "../utils/getService"; import { promiseOrTimeout } from "../utils/promise"; +import { segmentOverlapping } from "../utils/segments"; async function prepareCategorySegments(req: Request, videoID: VideoID, service: Service, segments: DBSegment[], cache: SegmentCache = { shadowHiddenSegmentIPs: {} }, useCache: boolean): Promise { @@ -353,14 +354,7 @@ function splitPercentOverlap(groups: OverlappingSegmentGroup[]): OverlappingSegm const bestGroup = result.find((group) => { // At least one segment in the group must have high % overlap or the same action type // Since POI and Full video segments will always have <= 0 overlap, they will always be in their own groups - return group.segments.some((compareSegment) => { - const overlap = Math.min(segment.endTime, compareSegment.endTime) - Math.max(segment.startTime, compareSegment.startTime); - const overallDuration = Math.max(segment.endTime, compareSegment.endTime) - Math.min(segment.startTime, compareSegment.startTime); - const overlapPercent = overlap / overallDuration; - return (overlapPercent >= 0.1 && segment.actionType === compareSegment.actionType && segment.category === compareSegment.category && segment.actionType !== ActionType.Chapter) - || (overlapPercent >= 0.6 && segment.actionType !== compareSegment.actionType && segment.category === compareSegment.category) - || (overlapPercent >= 0.8 && segment.actionType === ActionType.Chapter && compareSegment.actionType === ActionType.Chapter); - }); + return group.segments.some((compareSegment) => segmentOverlapping(segment, compareSegment)); }); if (bestGroup) { diff --git a/src/routes/postSkipSegments.ts b/src/routes/postSkipSegments.ts index c90f94bd..a05943e4 100644 --- a/src/routes/postSkipSegments.ts +++ b/src/routes/postSkipSegments.ts @@ -22,6 +22,7 @@ import axios from "axios"; import { vote } from "./voteOnSponsorTime"; import { canSubmit } from "../utils/permissions"; import { getVideoDetails, videoDetails } from "../utils/getVideoDetails"; +import { CompareSegment, segmentOverlapping } from "../utils/segments"; type CheckResult = { pass: boolean, @@ -261,7 +262,7 @@ function fetchOverlappingSegmentCanditates(videoID: VideoID, service: Service, s [videoID, service], { useReplica: true } ); - default: + case ActionType.Chapter: return db.prepare( "all", `SELECT "startTime", "endTime", "votes", "locked", "category", "actionType", "description" FROM "sponsorTimes" @@ -270,6 +271,8 @@ function fetchOverlappingSegmentCanditates(videoID: VideoID, service: Service, s [videoID, service, segment.actionType], { useReplica: true } ); + default: + return new Promise((resolve) => resolve([])); } } @@ -278,30 +281,28 @@ async function checkSegmentOverlap(videoID: VideoID, service: Service, incomingS if (candidates.length === 0) return null; // Can't overlap if there are no segments + const errorEnding = (c: string) => `has already been submitted.\n` + + `If this is what you tried to submit, please make sure you have the ${c} category enabled and try refreshing the segment list.\n`; + if (incomingSegment.actionType === ActionType.Poi || incomingSegment.actionType === ActionType.Full) { const bestSegment = candidates[0]; // fetchOverlappingSegmentCanditates returns only one segment for these - return `\nA ${bestSegment.actionType} ${bestSegment.category} segment ${incomingSegment.actionType === ActionType.Poi ? `at ${getFormattedTime(bestSegment.startTime)} ` : ""}has already been submitted.\n` + - `If this is what you tried to submit, please make sure you have the ${bestSegment.category} category enabled and try refreshing the segment list.\n`; + return `\nA ${bestSegment.actionType} ${bestSegment.category} segment ${incomingSegment.actionType === ActionType.Poi ? `at ${getFormattedTime(bestSegment.startTime)} ` : ""}${errorEnding(bestSegment.category)}`; } - const startTime = parseFloat(incomingSegment.segment[0]); - const endTime = parseFloat(incomingSegment.segment[1]); - const bestSegment = candidates.find((segment) => { - const overlap = Math.min(segment.endTime, endTime) - Math.max(segment.startTime, startTime); - const overallDuration = Math.max(segment.endTime, endTime) - Math.min(segment.startTime, startTime); - const overlapPercent = overlap / overallDuration; - return (overlapPercent >= 0.1 && segment.actionType === incomingSegment.actionType && segment.actionType !== ActionType.Chapter) - || (overlapPercent >= 0.6 && segment.actionType !== incomingSegment.actionType) - || (overlapPercent >= 0.8 && segment.actionType === ActionType.Chapter && incomingSegment.actionType === ActionType.Chapter); - }); + const compareSegment: CompareSegment = { + startTime: parseFloat(incomingSegment.segment[0]), + endTime: parseFloat(incomingSegment.segment[1]), + category: incomingSegment.category, + actionType: incomingSegment.actionType, + }; + const bestSegment = candidates.find((segment) => segmentOverlapping(segment, compareSegment)); if (!bestSegment) return null; // Overlap not found - if (bestSegment.actionType === ActionType.Chapter) - return `\nA "${bestSegment.description}" chapter at ${getFormattedTime(bestSegment.startTime)}-${getFormattedTime(bestSegment.endTime)} has already been submitted.\n` + - `If this is what you tried to submit, please make sure you have the ${bestSegment.category} category enabled and try refreshing the segment list.\n`; + const segmentType = bestSegment.actionType === ActionType.Chapter + ? `"${bestSegment.description}" chapter` + : `${bestSegment.actionType} ${bestSegment.category} segment`; - return `\nA ${bestSegment.actionType} ${bestSegment.category} segment at ${getFormattedTime(bestSegment.startTime)}-${getFormattedTime(bestSegment.endTime)} has already been submitted.\n` + - `If this is what you tried to submit, please make sure you have the ${bestSegment.category} category enabled and try refreshing the segment list.\n`; + return `\nA ${segmentType} at ${getFormattedTime(bestSegment.startTime)}-${getFormattedTime(bestSegment.endTime)}${errorEnding(bestSegment.category)}`; } async function checkEachSegmentValid(rawIP: IPAddress, paramUserID: UserID, userID: HashedUserID, videoID: VideoID, @@ -336,9 +337,9 @@ async function checkEachSegmentValid(rawIP: IPAddress, paramUserID: UserID, user errorMessage: `Users have voted that new segments aren't needed for the following category: ` + `'${segments[i].category}'\n` + - `${lockedCategoryList[lockIndex].reason?.length !== 0 ? `\nReason: '${lockedCategoryList[lockIndex].reason}'` : ""}\n` + - (overlapMessage ?? (segments[i].category === "sponsor" ? "\nMaybe the segment you are submitting is a different category that you have not enabled and is not a sponsor. " + - "Categories that aren't sponsor, such as self-promotion can be enabled in the options.\n" : "")) + + `${lockedCategoryList[lockIndex].reason?.length !== 0 ? `\nReason: '${lockedCategoryList[lockIndex].reason}'` : ``}\n` + + `${overlapMessage ?? (segments[i].category === "sponsor" ? `\nMaybe the segment you are submitting is a different category that you have not enabled and is not a sponsor. ` + + `Categories that aren't sponsor, such as self-promotion can be enabled in the options.\n` : ``)}` + `\nIf you believe this is incorrect, please contact someone on chat.sponsor.ajay.app, discord.gg/SponsorBlock or matrix.to/#/#sponsor:ajay.app` }; } diff --git a/src/utils/segments.ts b/src/utils/segments.ts new file mode 100644 index 00000000..5941e910 --- /dev/null +++ b/src/utils/segments.ts @@ -0,0 +1,17 @@ +import { ActionType, Category } from "../types/segments.model"; + +export interface CompareSegment { + startTime: number; + endTime: number; + category: Category; + actionType: ActionType; +} + +export function segmentOverlapping(segment1: CompareSegment, segment2: CompareSegment): boolean { + const overlap = Math.min(segment1.endTime, segment2.endTime) - Math.max(segment1.startTime, segment2.startTime); + const overallDuration = Math.max(segment1.endTime, segment2.endTime) - Math.min(segment1.startTime, segment2.startTime); + const overlapPercent = overlap / overallDuration; + return (overlapPercent >= 0.1 && segment1.actionType === segment2.actionType && segment1.category === segment2.category && segment1.actionType !== ActionType.Chapter) + || (overlapPercent >= 0.6 && segment1.actionType !== segment2.actionType && segment1.category === segment2.category) + || (overlapPercent >= 0.8 && segment1.actionType === ActionType.Chapter && segment2.actionType === ActionType.Chapter); +} \ No newline at end of file