Add submitting description for chapters

This commit is contained in:
Ajay Ramachandran 2021-11-06 00:54:28 -04:00
parent 0c16448065
commit 32150e4a1d
8 changed files with 98 additions and 10 deletions

View file

@ -46,7 +46,6 @@
] ]
} }
], ],
"categoryList": ["sponsor", "selfpromo", "interaction", "intro", "outro", "preview", "music_offtopic", "poi_highlight"],
"maxNumberOfActiveWarnings": 3, "maxNumberOfActiveWarnings": 3,
"hoursAfterWarningExpires": 24, "hoursAfterWarningExpires": 24,
"rateLimit": { "rateLimit": {

View file

@ -0,0 +1,8 @@
BEGIN TRANSACTION;
ALTER TABLE "sponsorTimes" ADD "description" TEXT NOT NULL default '';
ALTER TABLE "archivedSponsorTimes" ADD "description" TEXT NOT NULL default '';
UPDATE "config" SET value = 27 WHERE key = 'version';
COMMIT;

View file

@ -19,7 +19,7 @@ addDefaults(config, {
privateDBSchema: "./databases/_private.db.sql", privateDBSchema: "./databases/_private.db.sql",
readOnly: false, readOnly: false,
webhooks: [], webhooks: [],
categoryList: ["sponsor", "selfpromo", "interaction", "intro", "outro", "preview", "music_offtopic", "poi_highlight"], categoryList: ["sponsor", "selfpromo", "interaction", "intro", "outro", "preview", "music_offtopic", "poi_highlight", "chapter"],
categorySupport: { categorySupport: {
sponsor: ["skip", "mute"], sponsor: ["skip", "mute"],
selfpromo: ["skip", "mute"], selfpromo: ["skip", "mute"],
@ -29,6 +29,7 @@ addDefaults(config, {
preview: ["skip"], preview: ["skip"],
music_offtopic: ["skip"], music_offtopic: ["skip"],
poi_highlight: ["skip"], poi_highlight: ["skip"],
chapter: ["chapter"]
}, },
maxNumberOfActiveWarnings: 1, maxNumberOfActiveWarnings: 1,
hoursAfterWarningExpires: 24, hoursAfterWarningExpires: 24,

View file

@ -299,7 +299,7 @@ async function checkUserActiveWarning(userID: string): Promise<CheckResult> {
return CHECK_PASS; return CHECK_PASS;
} }
function checkInvalidFields(videoID: any, userID: any, segments: Array<any>): CheckResult { function checkInvalidFields(videoID: VideoID, userID: UserID, segments: IncomingSegment[]): CheckResult {
const invalidFields = []; const invalidFields = [];
const errors = []; const errors = [];
if (typeof videoID !== "string") { if (typeof videoID !== "string") {
@ -320,6 +320,12 @@ function checkInvalidFields(videoID: any, userID: any, segments: Array<any>): Ch
(typeof endTime === "string" && endTime.includes(":"))) { (typeof endTime === "string" && endTime.includes(":"))) {
invalidFields.push("segment time"); invalidFields.push("segment time");
} }
if (typeof segmentPair.description !== "string"
|| (segmentPair.description.length > 60 && segmentPair.actionType === ActionType.Chapter)
|| (segmentPair.description.length !== 0 && segmentPair.actionType !== ActionType.Chapter)) {
invalidFields.push("segment description");
}
} }
if (invalidFields.length !== 0) { if (invalidFields.length !== 0) {
@ -541,7 +547,8 @@ function preprocessInput(req: Request) {
segments = [{ segments = [{
segment: [req.query.startTime as string, req.query.endTime as string], segment: [req.query.startTime as string, req.query.endTime as string],
category: req.query.category as Category, category: req.query.category as Category,
actionType: (req.query.actionType as ActionType) ?? ActionType.Skip actionType: (req.query.actionType as ActionType) ?? ActionType.Skip,
description: req.query.description as string || "",
}]; }];
} }
// Add default action type // Add default action type
@ -550,6 +557,7 @@ function preprocessInput(req: Request) {
segment.actionType = ActionType.Skip; segment.actionType = ActionType.Skip;
} }
segment.description ??= "";
segment.segment = segment.segment.map((time) => typeof segment.segment[0] === "string" ? time?.replace(",", ".") : time); segment.segment = segment.segment.map((time) => typeof segment.segment[0] === "string" ? time?.replace(",", ".") : time);
}); });
@ -633,9 +641,10 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
const startingLocked = isVIP ? 1 : 0; const startingLocked = isVIP ? 1 : 0;
try { try {
await db.prepare("run", `INSERT INTO "sponsorTimes" await db.prepare("run", `INSERT INTO "sponsorTimes"
("videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "timeSubmitted", "views", "category", "actionType", "service", "videoDuration", "reputation", "shadowHidden", "hashedVideoID", "userAgent") ("videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "timeSubmitted", "views", "category", "actionType", "service", "videoDuration", "reputation", "shadowHidden", "hashedVideoID", "userAgent", "description")
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
videoID, segmentInfo.segment[0], segmentInfo.segment[1], startingVotes, startingLocked, UUID, userID, timeSubmitted, 0, segmentInfo.category, segmentInfo.actionType, service, videoDuration, reputation, shadowBanRow.userCount, hashedVideoID, userAgent videoID, segmentInfo.segment[0], segmentInfo.segment[1], startingVotes, startingLocked, UUID, userID, timeSubmitted, 0
, segmentInfo.category, segmentInfo.actionType, service, videoDuration, reputation, shadowBanRow.userCount, hashedVideoID, userAgent, segmentInfo.description
], ],
); );

View file

@ -5,7 +5,7 @@ import { UserID } from "./user.model";
export type SegmentUUID = string & { __segmentUUIDBrand: unknown }; export type SegmentUUID = string & { __segmentUUIDBrand: unknown };
export type VideoID = string & { __videoIDBrand: unknown }; export type VideoID = string & { __videoIDBrand: unknown };
export type VideoDuration = number & { __videoDurationBrand: unknown }; export type VideoDuration = number & { __videoDurationBrand: unknown };
export type Category = ("sponsor" | "selfpromo" | "interaction" | "intro" | "outro" | "preview" | "music_offtopic" | "poi_highlight") & { __categoryBrand: unknown }; export type Category = ("sponsor" | "selfpromo" | "interaction" | "intro" | "outro" | "preview" | "music_offtopic" | "poi_highlight" | "chapter") & { __categoryBrand: unknown };
export type VideoIDHash = VideoID & HashedValue; export type VideoIDHash = VideoID & HashedValue;
export type IPAddress = string & { __ipAddressBrand: unknown }; export type IPAddress = string & { __ipAddressBrand: unknown };
export type HashedIP = IPAddress & HashedValue; export type HashedIP = IPAddress & HashedValue;
@ -13,6 +13,7 @@ export type HashedIP = IPAddress & HashedValue;
export enum ActionType { export enum ActionType {
Skip = "skip", Skip = "skip",
Mute = "mute", Mute = "mute",
Chapter = "chapter"
} }
// Uncomment as needed // Uncomment as needed
@ -30,6 +31,7 @@ export interface IncomingSegment {
category: Category; category: Category;
actionType: ActionType; actionType: ActionType;
segment: string[]; segment: string[];
description?: string;
} }
export interface Segment { export interface Segment {

View file

@ -42,7 +42,6 @@
] ]
} }
], ],
"categoryList": ["sponsor", "selfpromo", "interaction", "intro", "outro", "preview", "music_offtopic", "poi_highlight"],
"maxNumberOfActiveWarnings": 3, "maxNumberOfActiveWarnings": 3,
"hoursAfterWarningExpires": 24, "hoursAfterWarningExpires": 24,
"rateLimit": { "rateLimit": {

View file

@ -108,7 +108,8 @@ describe("getLockReason", () => {
{ category: "outro", locked: 1, reason: "outro-reason", userID: vipUserID2, userName: vipUserName2 }, { category: "outro", locked: 1, reason: "outro-reason", userID: vipUserID2, userName: vipUserName2 },
{ category: "preview", locked: 1, reason: "preview-reason", userID: vipUserID1, userName: vipUserName1 }, { category: "preview", locked: 1, reason: "preview-reason", userID: vipUserID1, userName: vipUserName1 },
{ category: "music_offtopic", locked: 1, reason: "nonmusic-reason", userID: vipUserID1, userName: vipUserName1 }, { category: "music_offtopic", locked: 1, reason: "nonmusic-reason", userID: vipUserID1, userName: vipUserName1 },
{ category: "poi_highlight", locked: 0, reason: "", userID: "", userName: "" } { category: "poi_highlight", locked: 0, reason: "", userID: "", userName: "" },
{ category: "chapter", locked: 0, reason: "", userID: "", userName: "" }
]; ];
assert.deepStrictEqual(res.data, expected); assert.deepStrictEqual(res.data, expected);
done(); done();

View file

@ -37,6 +37,7 @@ describe("postSkipSegments", () => {
const queryDatabase = (videoID: string) => db.prepare("get", `SELECT "startTime", "endTime", "locked", "category" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]); const queryDatabase = (videoID: string) => db.prepare("get", `SELECT "startTime", "endTime", "locked", "category" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]);
const queryDatabaseActionType = (videoID: string) => db.prepare("get", `SELECT "startTime", "endTime", "locked", "category", "actionType" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]); const queryDatabaseActionType = (videoID: string) => db.prepare("get", `SELECT "startTime", "endTime", "locked", "category", "actionType" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]);
const queryDatabaseChapter = (videoID: string) => db.prepare("get", `SELECT "startTime", "endTime", "locked", "category", "actionType", "description" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]);
const queryDatabaseDuration = (videoID: string) => db.prepare("get", `SELECT "startTime", "endTime", "locked", "category", "videoDuration" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]); const queryDatabaseDuration = (videoID: string) => db.prepare("get", `SELECT "startTime", "endTime", "locked", "category", "videoDuration" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]);
const queryDatabaseVideoInfo = (videoID: string) => db.prepare("get", `SELECT * FROM "videoInfo" WHERE "videoID" = ?`, [videoID]); const queryDatabaseVideoInfo = (videoID: string) => db.prepare("get", `SELECT * FROM "videoInfo" WHERE "videoID" = ?`, [videoID]);
@ -181,6 +182,34 @@ describe("postSkipSegments", () => {
.catch(err => done(err)); .catch(err => done(err));
}); });
it("Should be able to submit a single chapter (JSON method)", (done) => {
const videoID = "postSkipChapter1";
postSkipSegmentJSON({
userID: submitUserOne,
videoID,
segments: [{
segment: [0, 10],
category: "chapter",
actionType: "chapter",
description: "This is a chapter"
}],
})
.then(async res => {
assert.strictEqual(res.status, 200);
const row = await queryDatabaseChapter(videoID);
const expected = {
startTime: 0,
endTime: 10,
category: "chapter",
actionType: "chapter",
description: "This is a chapter"
};
assert.ok(partialDeepEquals(row, expected));
done();
})
.catch(err => done(err));
});
it("Should not be able to submit an intro with mute action type (JSON method)", (done) => { it("Should not be able to submit an intro with mute action type (JSON method)", (done) => {
const videoID = "postSkip4"; const videoID = "postSkip4";
postSkipSegmentJSON({ postSkipSegmentJSON({
@ -201,6 +230,46 @@ describe("postSkipSegments", () => {
.catch(err => done(err)); .catch(err => done(err));
}); });
it("Should not be able to submit a chapter with skip action type (JSON method)", (done) => {
const videoID = "postSkipChapter2";
postSkipSegmentJSON({
userID: submitUserOne,
videoID,
segments: [{
segment: [0, 10],
category: "chapter",
actionType: "skip"
}],
})
.then(async res => {
assert.strictEqual(res.status, 400);
const row = await queryDatabaseActionType(videoID);
assert.strictEqual(row, undefined);
done();
})
.catch(err => done(err));
});
it("Should not be able to submit a sponsor with a description (JSON method)", (done) => {
const videoID = "postSkipChapter3";
postSkipSegmentJSON({
userID: submitUserOne,
videoID,
segments: [{
segment: [0, 10],
category: "sponsor",
description: "This is a sponsor"
}],
})
.then(async res => {
assert.strictEqual(res.status, 400);
const row = await queryDatabaseActionType(videoID);
assert.strictEqual(row, undefined);
done();
})
.catch(err => done(err));
});
it("Should be able to submit a single time with a duration from the YouTube API (JSON method)", (done) => { it("Should be able to submit a single time with a duration from the YouTube API (JSON method)", (done) => {
const videoID = "postSkip5"; const videoID = "postSkip5";
postSkipSegmentJSON({ postSkipSegmentJSON({