diff --git a/databases/_upgrade_sponsorTimes_45.sql b/databases/_upgrade_sponsorTimes_45.sql new file mode 100644 index 00000000..d50e2d9a --- /dev/null +++ b/databases/_upgrade_sponsorTimes_45.sql @@ -0,0 +1,15 @@ +BEGIN TRANSACTION; + +ALTER TABLE "sponsorTimes" ADD "cropLeft" INTEGER; +ALTER TABLE "sponsorTimes" ADD "cropRight" INTEGER; +ALTER TABLE "sponsorTimes" ADD "cropTop" INTEGER; +ALTER TABLE "sponsorTimes" ADD "cropBottom" INTEGER; + +ALTER TABLE "archivedSponsorTimes" ADD "cropLeft" INTEGER; +ALTER TABLE "archivedSponsorTimes" ADD "cropRight" INTEGER; +ALTER TABLE "archivedSponsorTimes" ADD "cropTop" INTEGER; +ALTER TABLE "archivedSponsorTimes" ADD "cropBottom" INTEGER; + +UPDATE "config" SET value = 45 WHERE key = 'version'; + +COMMIT; diff --git a/src/config.ts b/src/config.ts index c290a6f4..090127e6 100644 --- a/src/config.ts +++ b/src/config.ts @@ -19,7 +19,7 @@ addDefaults(config, { privateDBSchema: "./databases/_private.db.sql", readOnly: false, webhooks: [], - categoryList: ["sponsor", "selfpromo", "exclusive_access", "interaction", "intro", "outro", "preview", "music_offtopic", "filler", "poi_highlight", "chapter"], + categoryList: ["sponsor", "selfpromo", "exclusive_access", "interaction", "intro", "outro", "preview", "music_offtopic", "filler", "poi_highlight", "chapter", "windowbox"], casualCategoryList: ["funny", "creative", "clever", "descriptive", "other"], categorySupport: { sponsor: ["skip", "mute", "full"], @@ -32,7 +32,8 @@ addDefaults(config, { filler: ["skip", "mute"], music_offtopic: ["skip"], poi_highlight: ["poi"], - chapter: ["chapter"] + chapter: ["chapter", "crop"], + windowbox: ["full", "crop"] }, deArrowTypes: ["title", "thumbnail"], maxTitleLength: 110, diff --git a/src/routes/getSkipSegments.ts b/src/routes/getSkipSegments.ts index e21305cf..7ec26cce 100644 --- a/src/routes/getSkipSegments.ts +++ b/src/routes/getSkipSegments.ts @@ -218,7 +218,7 @@ async function getSegmentsFromDBByHash(hashedVideoIDPrefix: VideoIDHash, service const fetchFromDB = () => db .prepare( "all", - `SELECT "videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "category", "actionType", "videoDuration", "hidden", "reputation", "shadowHidden", "hashedVideoID", "timeSubmitted", "description" FROM "sponsorTimes" + `SELECT "videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "category", "actionType", "videoDuration", "hidden", "reputation", "shadowHidden", "hashedVideoID", "timeSubmitted", "description", "cropLeft", "cropRight", "cropTop", "cropBottom" FROM "sponsorTimes" WHERE "hashedVideoID" LIKE ? AND "service" = ? ORDER BY "startTime"`, [`${hashedVideoIDPrefix}%`, service], { useReplica: true } @@ -237,7 +237,7 @@ async function getSegmentsFromDBByVideoID(videoID: VideoID, service: Service): P const fetchFromDB = () => db .prepare( "all", - `SELECT "startTime", "endTime", "votes", "locked", "UUID", "userID", "category", "actionType", "videoDuration", "hidden", "reputation", "shadowHidden", "timeSubmitted", "description" FROM "sponsorTimes" + `SELECT "startTime", "endTime", "votes", "locked", "UUID", "userID", "category", "actionType", "videoDuration", "hidden", "reputation", "shadowHidden", "timeSubmitted", "description", "cropLeft", "cropRight", "cropTop", "cropBottom" FROM "sponsorTimes" WHERE "videoID" = ? AND "service" = ? ORDER BY "startTime"`, [videoID, service], { useReplica: true } diff --git a/src/routes/postSkipSegments.ts b/src/routes/postSkipSegments.ts index 8759b1e0..2e655141 100644 --- a/src/routes/postSkipSegments.ts +++ b/src/routes/postSkipSegments.ts @@ -302,6 +302,34 @@ async function checkEachSegmentValid(rawIP: IPAddress, paramUserID: UserID, user return { pass: false, errorMessage: "ActionType is not supported with this category.", errorCode: 400 }; } + if (segments[i].actionType === ActionType.Crop) { + const cropFields = [ + { name: 'cropLeft', value: segments[i].cropLeft }, + { name: 'cropRight', value: segments[i].cropRight }, + { name: 'cropTop', value: segments[i].cropTop }, + { name: 'cropBottom', value: segments[i].cropBottom } + ]; + for (const field of cropFields) { + if (field.value !== undefined) { + if (!Number.isInteger(field.value) || field.value < 0 || field.value > 255) { + return { pass: false, errorMessage: `${field.name} must be an integer between 0 and 255.`, errorCode: 400 }; + } + } + } + + const leftValue = segments[i].cropLeft || 0; + const rightValue = segments[i].cropRight || 0; + const topValue = segments[i].cropTop || 0; + const bottomValue = segments[i].cropBottom || 0; + + if (leftValue + rightValue > 256) { + return { pass: false, errorMessage: "cropLeft + cropRight must not exceed 256.", errorCode: 400 }; + } + if (topValue + bottomValue > 256) { + return { pass: false, errorMessage: "cropTop + cropBottom must not exceed 256.", errorCode: 400 }; + } + } + const startTime = parseFloat(segments[i].segment[0]); const endTime = parseFloat(segments[i].segment[1]); @@ -612,10 +640,10 @@ export async function postSkipSegments(req: Request, res: Response): Promise