diff --git a/DatabaseSchema.md b/DatabaseSchema.md index b3484f4..a9f4344 100644 --- a/DatabaseSchema.md +++ b/DatabaseSchema.md @@ -44,6 +44,7 @@ | shadowHidden | INTEGER | not null | | hashedVideoID | TEXT | not null, default '', sha256 | | userAgent | TEXT | not null, default '' | +| description | TEXT | not null, default '' | | index | field | | -- | :--: | diff --git a/ci.json b/ci.json index 32ef176..2f948b7 100644 --- a/ci.json +++ b/ci.json @@ -46,7 +46,6 @@ ] } ], - "categoryList": ["sponsor", "selfpromo", "interaction", "intro", "outro", "preview", "music_offtopic", "poi_highlight"], "maxNumberOfActiveWarnings": 3, "hoursAfterWarningExpires": 24, "rateLimit": { diff --git a/databases/_sponsorTimes_indexes.sql b/databases/_sponsorTimes_indexes.sql index 968558f..4a353d0 100644 --- a/databases/_sponsorTimes_indexes.sql +++ b/databases/_sponsorTimes_indexes.sql @@ -25,6 +25,11 @@ CREATE INDEX IF NOT EXISTS "sponsorTimes_videoID" ("videoID" COLLATE pg_catalog."default" ASC NULLS LAST, service COLLATE pg_catalog."default" ASC NULLS LAST, category COLLATE pg_catalog."default" ASC NULLS LAST, "timeSubmitted" ASC NULLS LAST) TABLESPACE pg_default; +CREATE INDEX IF NOT EXISTS "sponsorTimes_description_gin" + ON public."sponsorTimes" USING gin + ("description" COLLATE pg_catalog."default" gin_trgm_ops, category COLLATE pg_catalog."default" gin_trgm_ops) + TABLESPACE pg_default; + -- userNames CREATE INDEX IF NOT EXISTS "userNames_userID" diff --git a/databases/_upgrade_sponsorTimes_27.sql b/databases/_upgrade_sponsorTimes_27.sql new file mode 100644 index 0000000..8887875 --- /dev/null +++ b/databases/_upgrade_sponsorTimes_27.sql @@ -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; \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index 974805e..fbb7d2c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -42,6 +42,7 @@ import { getUserStats } from "./routes/getUserStats"; import ExpressPromiseRouter from "express-promise-router"; import { Server } from "http"; import { youtubeApiProxy } from "./routes/youtubeApiProxy"; +import { getChapterNames } from "./routes/getChapterNames"; export function createServer(callback: () => void): Server { // Create a service (the app object is just a callback). @@ -172,6 +173,9 @@ function setupRoutes(router: Router) { // get all segments that match a search router.get("/api/searchSegments", getSearchSegments); + // autocomplete chapter names + router.get("/api/chapterNames", getChapterNames); + // get status router.get("/api/status/:value", getStatus); router.get("/api/status", getStatus); diff --git a/src/config.ts b/src/config.ts index d39280f..b60aa21 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", "interaction", "intro", "outro", "preview", "music_offtopic", "filler", "poi_highlight"], + categoryList: ["sponsor", "selfpromo", "interaction", "intro", "outro", "preview", "music_offtopic", "filler", "poi_highlight", "chapter"], categorySupport: { sponsor: ["skip", "mute"], selfpromo: ["skip", "mute"], @@ -30,6 +30,7 @@ addDefaults(config, { filler: ["skip", "mute"], music_offtopic: ["skip"], poi_highlight: ["skip"], + chapter: ["chapter"] }, maxNumberOfActiveWarnings: 1, hoursAfterWarningExpires: 24, diff --git a/src/databases/Postgres.ts b/src/databases/Postgres.ts index 5c2b5c5..79d2adb 100644 --- a/src/databases/Postgres.ts +++ b/src/databases/Postgres.ts @@ -68,7 +68,7 @@ export class Postgres implements IDatabase { } case "all": { const values = queryResult.rows; - Logger.debug(`result (postgres): ${values}`); + Logger.debug(`result (postgres): ${JSON.stringify(values)}`); return values; } case "run": { diff --git a/src/routes/getChapterNames.ts b/src/routes/getChapterNames.ts new file mode 100644 index 0000000..1b04e5b --- /dev/null +++ b/src/routes/getChapterNames.ts @@ -0,0 +1,46 @@ +import { Logger } from "../utils/logger"; +import { Request, Response } from "express"; +import { db } from "../databases/databases"; +import { Postgres } from "../databases/Postgres"; + +export async function getChapterNames(req: Request, res: Response): Promise { + const description = req.query.description as string; + const channelID = req.query.channelID as string; + + if (!description || typeof(description) !== "string" + || !channelID || typeof(channelID) !== "string") { + return res.sendStatus(400); + } + + if (!(db instanceof Postgres)) { + return res.sendStatus(500).json({ + message: "Not supported on this instance" + }); + } + + try { + const descriptions = await db.prepare("all", ` + SELECT "description" + FROM "sponsorTimes" + WHERE ("votes" > 0 OR ("views" > 100 AND "votes" >= 0)) AND "videoID" IN ( + SELECT "videoID" + FROM "videoInfo" + WHERE "channelID" = ? + ) AND "description" != '' + GROUP BY "description" + ORDER BY SUM("votes"), similarity("description", ?) DESC + LIMIT 5;` + , [channelID, description]) as { description: string }[]; + + if (descriptions?.length > 0) { + return res.status(200).json(descriptions.map(d => ({ + description: d.description + }))); + } + } catch (err) { + Logger.error(err as string); + return res.sendStatus(500); + } + + return res.status(404).json([]); +} diff --git a/src/routes/getSkipSegments.ts b/src/routes/getSkipSegments.ts index c5ae3de..b8b0e71 100644 --- a/src/routes/getSkipSegments.ts +++ b/src/routes/getSkipSegments.ts @@ -45,7 +45,7 @@ async function prepareCategorySegments(req: Request, videoID: VideoID, category: const filteredSegments = segments.filter((_, index) => shouldFilter[index]); - const maxSegments = getCategoryActionType(category) === CategoryActionType.Skippable ? 32 : 1; + const maxSegments = getCategoryActionType(category) === CategoryActionType.Skippable ? Infinity : 1; return (await chooseSegments(filteredSegments, maxSegments)).map((chosenSegment) => ({ category: chosenSegment.category, actionType: chosenSegment.actionType, @@ -55,6 +55,7 @@ async function prepareCategorySegments(req: Request, videoID: VideoID, category: votes: chosenSegment.votes, videoDuration: chosenSegment.videoDuration, userID: chosenSegment.userID, + description: chosenSegment.description })); } @@ -139,7 +140,7 @@ async function getSegmentsFromDBByHash(hashedVideoIDPrefix: VideoIDHash, service const fetchFromDB = () => db .prepare( "all", - `SELECT "videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "category", "actionType", "videoDuration", "reputation", "shadowHidden", "hashedVideoID", "timeSubmitted" FROM "sponsorTimes" + `SELECT "videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "category", "actionType", "videoDuration", "reputation", "shadowHidden", "hashedVideoID", "timeSubmitted", "description" FROM "sponsorTimes" WHERE "hashedVideoID" LIKE ? AND "service" = ? AND "hidden" = 0 ORDER BY "startTime"`, [`${hashedVideoIDPrefix}%`, service] ) as Promise; @@ -155,7 +156,7 @@ async function getSegmentsFromDBByVideoID(videoID: VideoID, service: Service): P const fetchFromDB = () => db .prepare( "all", - `SELECT "startTime", "endTime", "votes", "locked", "UUID", "userID", "category", "actionType", "videoDuration", "reputation", "shadowHidden", "timeSubmitted" FROM "sponsorTimes" + `SELECT "startTime", "endTime", "votes", "locked", "UUID", "userID", "category", "actionType", "videoDuration", "reputation", "shadowHidden", "timeSubmitted", "description" FROM "sponsorTimes" WHERE "videoID" = ? AND "service" = ? AND "hidden" = 0 ORDER BY "startTime"`, [videoID, service] ) as Promise; @@ -219,7 +220,7 @@ async function chooseSegments(segments: DBSegment[], max: number): Promise cursor), no other segment will ever fall // inside that group (because they're sorted) so we can create a new one - const overlappingSegmentsGroups: OverlappingSegmentGroup[] = []; + let overlappingSegmentsGroups: OverlappingSegmentGroup[] = []; let currentGroup: OverlappingSegmentGroup; let cursor = -1; //-1 to make sure that, even if the 1st segment starts at 0, a new group is created for (const segment of segments) { @@ -261,6 +262,8 @@ async function chooseSegments(segments: DBSegment[], max: number): Promise { + const result: OverlappingSegmentGroup[] = []; + group.segments.forEach((segment) => { + const bestGroup = result.find((group) => { + // At least one segment in the group must have high % overlap or the same action type + 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 && segment.actionType === compareSegment.actionType && segment.actionType !== ActionType.Chapter) + || overlapPercent >= 0.6 + || (overlapPercent >= 0.8 && segment.actionType === ActionType.Chapter && compareSegment.actionType === ActionType.Chapter); + }); + }); + + if (bestGroup) { + bestGroup.segments.push(segment); + bestGroup.votes += segment.votes; + bestGroup.reputation += segment.reputation; + bestGroup.locked ||= segment.locked; + bestGroup.required ||= segment.required; + } else { + result.push({ segments: [segment], votes: segment.votes, reputation: segment.reputation, locked: segment.locked, required: segment.required }); + } + }); + + return result; + }); +} + /** * * Returns what would be sent to the client. diff --git a/src/routes/postSkipSegments.ts b/src/routes/postSkipSegments.ts index c65e770..09b6c06 100644 --- a/src/routes/postSkipSegments.ts +++ b/src/routes/postSkipSegments.ts @@ -299,7 +299,7 @@ async function checkUserActiveWarning(userID: string): Promise { return CHECK_PASS; } -function checkInvalidFields(videoID: any, userID: any, segments: Array): CheckResult { +function checkInvalidFields(videoID: VideoID, userID: UserID, segments: IncomingSegment[]): CheckResult { const invalidFields = []; const errors = []; if (typeof videoID !== "string") { @@ -320,6 +320,12 @@ function checkInvalidFields(videoID: any, userID: any, segments: Array): Ch (typeof endTime === "string" && endTime.includes(":"))) { 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) { @@ -541,7 +547,8 @@ function preprocessInput(req: Request) { segments = [{ segment: [req.query.startTime as string, req.query.endTime as string], 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 @@ -550,6 +557,7 @@ function preprocessInput(req: Request) { segment.actionType = ActionType.Skip; } + segment.description ??= ""; 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 { + const query = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "timeSubmitted", "views", "category", "actionType", "service", "videoDuration", "hidden", "shadowHidden", "description") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; + await db.prepare("run", query, [chapterNamesVid1, 60, 80, 2, 0, "chapterNamesVid-1", "testman", 0, 50, "chapter", "chapter", "YouTube", 0, 0, 0, "Weird name"]); + await db.prepare("run", query, [chapterNamesVid1, 70, 75, 2, 0, "chapterNamesVid-2", "testman", 0, 50, "chapter", "chapter", "YouTube", 0, 0, 0, "A different one"]); + await db.prepare("run", query, [chapterNamesVid1, 71, 76, 2, 0, "chapterNamesVid-3", "testman", 0, 50, "chapter", "chapter", "YouTube", 0, 0, 0, "Something else"]); + + await db.prepare("run", `INSERT INTO "videoInfo" ("videoID", "channelID", "title", "published", "genreUrl") + SELECT ?, ?, ?, ?, ?`, [ + chapterNamesVid1, chapterChannelID, "", 0, "" + ]); + }); + + it("Search for 'weird'", async () => { + const result = await client.get(`${endpoint}?description=weird&channelID=${chapterChannelID}`); + const expected = [{ + description: "Weird name", + }]; + + assert.strictEqual(result.status, 200); + assert.strictEqual(result.data.length, 3); + assert.ok(partialDeepEquals(result.data, expected)); + }); + + it("Search for 'different'", async () => { + const result = await client.get(`${endpoint}?description=different&channelID=${chapterChannelID}`); + const expected = [{ + description: "A different one", + }]; + + assert.strictEqual(result.status, 200); + assert.strictEqual(result.data.length, 3); + assert.ok(partialDeepEquals(result.data, expected)); + }); + + it("Search for 'something'", async () => { + const result = await client.get(`${endpoint}?description=something&channelID=${chapterChannelID}`); + const expected = [{ + description: "Something else", + }]; + + assert.strictEqual(result.status, 200); + assert.strictEqual(result.data.length, 3); + assert.ok(partialDeepEquals(result.data, expected)); + }); + }); +} \ No newline at end of file diff --git a/test/cases/getLockReason.ts b/test/cases/getLockReason.ts index bf517d5..c5bdb89 100644 --- a/test/cases/getLockReason.ts +++ b/test/cases/getLockReason.ts @@ -108,7 +108,9 @@ describe("getLockReason", () => { { category: "outro", locked: 1, reason: "outro-reason", userID: vipUserID2, userName: vipUserName2 }, { category: "preview", locked: 1, reason: "preview-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: "filler", 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); done(); diff --git a/test/cases/getSkipSegments.ts b/test/cases/getSkipSegments.ts index 63eb208..9b1b38c 100644 --- a/test/cases/getSkipSegments.ts +++ b/test/cases/getSkipSegments.ts @@ -6,23 +6,26 @@ import { client } from "../utils/httpClient"; describe("getSkipSegments", () => { const endpoint = "/api/skipSegments"; before(async () => { - const query = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "timeSubmitted", "views", "category", "actionType", "service", "videoDuration", "hidden", "shadowHidden") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; - await db.prepare("run", query, ["getSkipSegmentID0", 1, 11, 1, 0, "uuid01", "testman", 0, 50, "sponsor", "skip", "YouTube", 100, 0, 0]); - await db.prepare("run", query, ["getSkipSegmentID0", 12, 14, 2, 0, "uuid02", "testman", 0, 50, "sponsor", "mute", "YouTube", 100, 0, 0]); - await db.prepare("run", query, ["getSkipSegmentID0", 20, 33, 2, 0, "uuid03", "testman", 0, 50, "intro", "skip", "YouTube", 101, 0, 0]); - await db.prepare("run", query, ["getSkipSegmentID1", 1, 11, 2, 0, "uuid10", "testman", 0, 50, "sponsor", "skip", "PeerTube", 120, 0, 0]); - await db.prepare("run", query, ["getSkipSegmentID2", 1, 11, 2, 1, "uuid20", "testman", 0, 50, "sponsor", "skip", "YouTube", 140, 0, 0]); - await db.prepare("run", query, ["getSkipSegmentID3", 1, 11, 2, 0, "uuid30", "testman", 0, 50, "sponsor", "skip", "YouTube", 200, 0, 0]); - await db.prepare("run", query, ["getSkipSegmentID3", 7, 22, -3, 0, "uuid31", "testman", 0, 50, "sponsor", "skip", "YouTube", 300, 0, 0]); - await db.prepare("run", query, ["getSkipSegmentMultiple", 1, 11, 2, 0, "uuid40", "testman", 0, 50, "intro", "skip", "YouTube", 400, 0, 0]); - await db.prepare("run", query, ["getSkipSegmentMultiple", 20, 33, 2, 0, "uuid41", "testman", 0, 50, "intro", "skip", "YouTube", 500, 0, 0]); - await db.prepare("run", query, ["getSkipSegmentLocked", 20, 33, 2, 1, "uuid50", "testman", 0, 50, "intro", "skip", "YouTube", 230, 0, 0]); - await db.prepare("run", query, ["getSkipSegmentLocked", 20, 34, 100000, 0, "uuid51", "testman", 0, 50, "intro", "skip", "YouTube", 190, 0, 0]); - await db.prepare("run", query, ["getSkipSegmentID6", 20, 34, 100000, 0, "uuid60", "testman", 0, 50, "sponsor", "skip", "YouTube", 190, 1, 0]); - await db.prepare("run", query, ["requiredSegmentVid", 60, 70, 2, 0, "requiredSegmentVid1", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, 0]); - await db.prepare("run", query, ["requiredSegmentVid", 60, 70, -2, 0, "requiredSegmentVid2", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, 0]); - await db.prepare("run", query, ["requiredSegmentVid", 80, 90, -2, 0, "requiredSegmentVid3", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, 0]); - await db.prepare("run", query, ["requiredSegmentVid", 80, 90, 2, 0, "requiredSegmentVid4", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, 0]); + const query = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "timeSubmitted", "views", "category", "actionType", "service", "videoDuration", "hidden", "shadowHidden", "description") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; + await db.prepare("run", query, ["getSkipSegmentID0", 1, 11, 1, 0, "uuid01", "testman", 0, 50, "sponsor", "skip", "YouTube", 100, 0, 0, ""]); + await db.prepare("run", query, ["getSkipSegmentID0", 12, 14, 2, 0, "uuid02", "testman", 0, 50, "sponsor", "mute", "YouTube", 100, 0, 0, ""]); + await db.prepare("run", query, ["getSkipSegmentID0", 20, 33, 2, 0, "uuid03", "testman", 0, 50, "intro", "skip", "YouTube", 101, 0, 0, ""]); + await db.prepare("run", query, ["getSkipSegmentID1", 1, 11, 2, 0, "uuid10", "testman", 0, 50, "sponsor", "skip", "PeerTube", 120, 0, 0, ""]); + await db.prepare("run", query, ["getSkipSegmentID2", 1, 11, 2, 1, "uuid20", "testman", 0, 50, "sponsor", "skip", "YouTube", 140, 0, 0, ""]); + await db.prepare("run", query, ["getSkipSegmentID3", 1, 11, 2, 0, "uuid30", "testman", 0, 50, "sponsor", "skip", "YouTube", 200, 0, 0, ""]); + await db.prepare("run", query, ["getSkipSegmentID3", 7, 22, -3, 0, "uuid31", "testman", 0, 50, "sponsor", "skip", "YouTube", 300, 0, 0, ""]); + await db.prepare("run", query, ["getSkipSegmentMultiple", 1, 11, 2, 0, "uuid40", "testman", 0, 50, "intro", "skip", "YouTube", 400, 0, 0, ""]); + await db.prepare("run", query, ["getSkipSegmentMultiple", 20, 33, 2, 0, "uuid41", "testman", 0, 50, "intro", "skip", "YouTube", 500, 0, 0, ""]); + await db.prepare("run", query, ["getSkipSegmentLocked", 20, 33, 2, 1, "uuid50", "testman", 0, 50, "intro", "skip", "YouTube", 230, 0, 0, ""]); + await db.prepare("run", query, ["getSkipSegmentLocked", 20, 34, 100000, 0, "uuid51", "testman", 0, 50, "intro", "skip", "YouTube", 190, 0, 0, ""]); + await db.prepare("run", query, ["getSkipSegmentID6", 20, 34, 100000, 0, "uuid60", "testman", 0, 50, "sponsor", "skip", "YouTube", 190, 1, 0, ""]); + await db.prepare("run", query, ["requiredSegmentVid", 60, 70, 2, 0, "requiredSegmentVid1", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, 0, ""]); + await db.prepare("run", query, ["requiredSegmentVid", 60, 70, -2, 0, "requiredSegmentVid2", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, 0, ""]); + await db.prepare("run", query, ["requiredSegmentVid", 80, 90, -2, 0, "requiredSegmentVid3", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, 0, ""]); + await db.prepare("run", query, ["requiredSegmentVid", 80, 90, 2, 0, "requiredSegmentVid4", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, 0, ""]); + await db.prepare("run", query, ["chapterVid", 60, 80, 2, 0, "chapterVid-1", "testman", 0, 50, "chapter", "chapter", "YouTube", 0, 0, 0, "Chapter 1"]); + await db.prepare("run", query, ["chapterVid", 70, 75, 2, 0, "chapterVid-2", "testman", 0, 50, "chapter", "chapter", "YouTube", 0, 0, 0, "Chapter 2"]); + await db.prepare("run", query, ["chapterVid", 71, 76, 2, 0, "chapterVid-3", "testman", 0, 50, "chapter", "chapter", "YouTube", 0, 0, 0, "Chapter 3"]); return; }); @@ -388,6 +391,33 @@ describe("getSkipSegments", () => { .catch(err => done(err)); }); + it("Should be able to get overlapping chapter segments if very different", (done) => { + client.get(`${endpoint}?videoID=chapterVid&category=chapter&actionType=chapter`) + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + assert.strictEqual(data.length, 2); + const expected = [{ + UUID: "chapterVid-1", + description: "Chapter 1" + }, { + UUID: "chapterVid-2", + description: "Chapter 2" + }]; + const expected2 = [{ + UUID: "chapterVid-1", + description: "Chapter 1" + }, { + UUID: "chapterVid-3", + description: "Chapter 3" + }]; + + assert.ok(partialDeepEquals(data, expected, false) || partialDeepEquals(data, expected2)); + done(); + }) + .catch(err => done(err)); + }); + it("Should get 400 if no videoID passed in", (done) => { client.get(endpoint) .then(res => { diff --git a/test/cases/getSkipSegmentsByHash.ts b/test/cases/getSkipSegmentsByHash.ts index fb83ec0..210008c 100644 --- a/test/cases/getSkipSegmentsByHash.ts +++ b/test/cases/getSkipSegmentsByHash.ts @@ -16,20 +16,31 @@ describe("getSkipSegmentsByHash", () => { const getSegmentsByHash0Hash = "fdaff4dee1043451faa7398324fb63d8618ebcd11bddfe0491c488db12c6c910"; const requiredSegmentVidHash = "d51822c3f681e07aef15a8855f52ad12db9eb9cf059e65b16b64c43359557f61"; before(async () => { - const query = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "UUID", "userID", "timeSubmitted", views, category, "actionType", "service", "hidden", "shadowHidden", "hashedVideoID") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; - await db.prepare("run", query, ["getSegmentsByHash-0", 1, 10, 2, "getSegmentsByHash-01", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, getSegmentsByHash0Hash]); - await db.prepare("run", query, ["getSegmentsByHash-0", 1, 10, 2, "getSegmentsByHash-02", "testman", 0, 50, "sponsor", "skip", "PeerTube", 0, 0, getSegmentsByHash0Hash]); - await db.prepare("run", query, ["getSegmentsByHash-0", 20, 30, 2, "getSegmentsByHash-03", "testman", 100, 150, "intro", "skip", "YouTube", 0, 0, getSegmentsByHash0Hash]); - await db.prepare("run", query, ["getSegmentsByHash-0", 40, 50, 2, "getSegmentsByHash-04", "testman", 0, 50, "sponsor", "mute", "YouTube", 0, 0, getSegmentsByHash0Hash]); - await db.prepare("run", query, ["getSegmentsByHash-noMatchHash", 40, 50, 2, "getSegmentsByHash-noMatchHash", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, "fdaffnoMatchHash"]); - await db.prepare("run", query, ["getSegmentsByHash-1", 60, 70, 2, "getSegmentsByHash-1", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, "3272fa85ee0927f6073ef6f07ad5f3146047c1abba794cfa364d65ab9921692b"]); - await db.prepare("run", query, ["onlyHidden", 60, 70, 2, "onlyHidden", "testman", 0, 50, "sponsor", "skip", "YouTube", 1, 0, "f3a199e1af001d716cdc6599360e2b062c2d2b3fa2885f6d9d2fd741166cbbd3"]); - await db.prepare("run", query, ["highlightVid", 60, 60, 2, "highlightVid-1", "testman", 0, 50, "poi_highlight", "skip", "YouTube", 0, 0, getHash("highlightVid", 1)]); - await db.prepare("run", query, ["highlightVid", 70, 70, 2, "highlightVid-2", "testman", 0, 50, "poi_highlight", "skip", "YouTube", 0, 0, getHash("highlightVid", 1)]); - await db.prepare("run", query, ["requiredSegmentVid", 60, 70, 2, "requiredSegmentVid-1", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, requiredSegmentVidHash]); - await db.prepare("run", query, ["requiredSegmentVid", 60, 70, -2, "requiredSegmentVid-2", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, requiredSegmentVidHash]); - await db.prepare("run", query, ["requiredSegmentVid", 80, 90, -2, "requiredSegmentVid-3", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, requiredSegmentVidHash]); - await db.prepare("run", query, ["requiredSegmentVid", 80, 90, 2, "requiredSegmentVid-4", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, requiredSegmentVidHash]); + const query = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "UUID", "userID", "timeSubmitted", views, category, "actionType", "service", "hidden", "shadowHidden", "hashedVideoID", "description") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; + await db.prepare("run", query, ["getSegmentsByHash-0", 1, 10, 2, "getSegmentsByHash-01", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, getSegmentsByHash0Hash, ""]); + await db.prepare("run", query, ["getSegmentsByHash-0", 1, 10, 2, "getSegmentsByHash-02", "testman", 0, 50, "sponsor", "skip", "PeerTube", 0, 0, getSegmentsByHash0Hash, ""]); + await db.prepare("run", query, ["getSegmentsByHash-0", 20, 30, 2, "getSegmentsByHash-03", "testman", 100, 150, "intro", "skip", "YouTube", 0, 0, getSegmentsByHash0Hash, ""]); + await db.prepare("run", query, ["getSegmentsByHash-0", 40, 50, 2, "getSegmentsByHash-04", "testman", 0, 50, "sponsor", "mute", "YouTube", 0, 0, getSegmentsByHash0Hash, ""]); + await db.prepare("run", query, ["getSegmentsByHash-noMatchHash", 40, 50, 2, "getSegmentsByHash-noMatchHash", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, "fdaffnoMatchHash", ""]); + await db.prepare("run", query, ["getSegmentsByHash-1", 60, 70, 2, "getSegmentsByHash-1", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, "3272fa85ee0927f6073ef6f07ad5f3146047c1abba794cfa364d65ab9921692b", ""]); + await db.prepare("run", query, ["onlyHidden", 60, 70, 2, "onlyHidden", "testman", 0, 50, "sponsor", "skip", "YouTube", 1, 0, "f3a199e1af001d716cdc6599360e2b062c2d2b3fa2885f6d9d2fd741166cbbd3", ""]); + await db.prepare("run", query, ["highlightVid", 60, 60, 2, "highlightVid-1", "testman", 0, 50, "poi_highlight", "skip", "YouTube", 0, 0, getHash("highlightVid", 1), ""]); + await db.prepare("run", query, ["highlightVid", 70, 70, 2, "highlightVid-2", "testman", 0, 50, "poi_highlight", "skip", "YouTube", 0, 0, getHash("highlightVid", 1), ""]); + await db.prepare("run", query, ["requiredSegmentVid", 60, 70, 2, "requiredSegmentVid-1", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, requiredSegmentVidHash, ""]); + await db.prepare("run", query, ["requiredSegmentVid", 60, 70, -2, "requiredSegmentVid-2", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, requiredSegmentVidHash, ""]); + await db.prepare("run", query, ["requiredSegmentVid", 80, 90, -2, "requiredSegmentVid-3", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, requiredSegmentVidHash, ""]); + await db.prepare("run", query, ["requiredSegmentVid", 80, 90, 2, "requiredSegmentVid-4", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, requiredSegmentVidHash, ""]); + await db.prepare("run", query, ["chapterVid-hash", 60, 80, 2, "chapterVid-hash-1", "testman", 0, 50, "chapter", "chapter", "YouTube", 0, 0, getHash("chapterVid-hash", 1), "Chapter 1"]); //7258 + await db.prepare("run", query, ["chapterVid-hash", 70, 75, 2, "chapterVid-hash-2", "testman", 0, 50, "chapter", "chapter", "YouTube", 0, 0, getHash("chapterVid-hash", 1), "Chapter 2"]); //7258 + await db.prepare("run", query, ["chapterVid-hash", 71, 76, 2, "chapterVid-hash-3", "testman", 0, 50, "chapter", "chapter", "YouTube", 0, 0, getHash("chapterVid-hash", 1), "Chapter 3"]); //7258 + await db.prepare("run", query, ["longMuteVid-hash", 40, 45, 2, "longMuteVid-hash-1", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, getHash("longMuteVid-hash", 1), ""]); //6613 + await db.prepare("run", query, ["longMuteVid-hash", 30, 35, 2, "longMuteVid-hash-2", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, getHash("longMuteVid-hash", 1), ""]); //6613 + await db.prepare("run", query, ["longMuteVid-hash", 2, 80, 2, "longMuteVid-hash-3", "testman", 0, 50, "sponsor", "mute", "YouTube", 0, 0, getHash("longMuteVid-hash", 1), ""]); //6613 + await db.prepare("run", query, ["longMuteVid-hash", 3, 78, 2, "longMuteVid-hash-4", "testman", 0, 50, "sponsor", "mute", "YouTube", 0, 0, getHash("longMuteVid-hash", 1), ""]); //6613 + await db.prepare("run", query, ["longMuteVid-2-hash", 1, 15, 2, "longMuteVid-2-hash-1", "testman", 0, 50, "sponsor", "mute", "YouTube", 0, 0, getHash("longMuteVid-2-hash", 1), ""]); //ab0c + await db.prepare("run", query, ["longMuteVid-2-hash", 30, 35, 2, "longMuteVid-2-hash-2", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, getHash("longMuteVid-2-hash", 1), ""]); //ab0c + await db.prepare("run", query, ["longMuteVid-2-hash", 2, 80, 2, "longMuteVid-2-hash-3", "testman", 0, 50, "sponsor", "mute", "YouTube", 0, 0, getHash("longMuteVid-2-hash", 1), ""]); //ab0c + await db.prepare("run", query, ["longMuteVid-2-hash", 3, 78, 2, "longMuteVid-2-hash-4", "testman", 0, 50, "sponsor", "mute", "YouTube", 0, 0, getHash("longMuteVid-2-hash", 1), ""]); //ab0c }); it("Should be able to get a 200", (done) => { @@ -356,4 +367,115 @@ describe("getSkipSegmentsByHash", () => { }) .catch(err => done(err)); }); + + it("Should be able to get overlapping chapter segments if very different", (done) => { + client.get(`${endpoint}/7258?category=chapter&actionType=chapter`) + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + assert.strictEqual(data.length, 1); + const expected = [{ + segments: [{ + UUID: "chapterVid-hash-1", + description: "Chapter 1" + }, { + UUID: "chapterVid-hash-2", + description: "Chapter 2" + }] + }]; + const expected2 = [{ + segments: [{ + UUID: "chapterVid-hash-1", + description: "Chapter 1" + }, { + UUID: "chapterVid-hash-3", + description: "Chapter 3" + }] + }]; + + assert.ok(partialDeepEquals(data, expected, false) || partialDeepEquals(data, expected2)); + assert.strictEqual(data[0].segments.length, 2); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to get mute segment with small skip segment in middle", (done) => { + client.get(`${endpoint}/6613?actionType=skip&actionType=mute`) + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + assert.strictEqual(data.length, 1); + const expected = [{ + segments: [{ + UUID: "longMuteVid-hash-3", + actionType: "mute" + }, { + UUID: "longMuteVid-hash-2", + actionType: "skip" + }, { + UUID: "longMuteVid-hash-1", + actionType: "skip" + }] + }]; + const expected2 = [{ + segments: [{ + UUID: "longMuteVid-hash-4", + actionType: "mute" + }, { + UUID: "longMuteVid-hash-2", + actionType: "skip" + }, { + UUID: "longMuteVid-hash-1", + actionType: "skip" + }] + }]; + + assert.ok(partialDeepEquals(data, expected, false) || partialDeepEquals(data, expected2)); + assert.strictEqual(data[0].segments.length, 3); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to get mute segment with small skip segment in middle (2)", (done) => { + client.get(`${endpoint}/ab0c?actionType=skip&actionType=mute`) + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + assert.strictEqual(data.length, 1); + const expected = [{ + segments: [{ + UUID: "longMuteVid-2-hash-1", + actionType: "mute" + }, { + UUID: "longMuteVid-2-hash-2", + actionType: "skip" + }] + }]; + const expected2 = [{ + segments: [{ + UUID: "longMuteVid-2-hash-3", + actionType: "mute" + }, { + UUID: "longMuteVid-2-hash-2", + actionType: "skip" + }] + }]; + const expected3 = [{ + segments: [{ + UUID: "longMuteVid-2-hash-4", + actionType: "mute" + }, { + UUID: "longMuteVid-2-hash-2", + actionType: "skip" + }] + }]; + + assert.ok(partialDeepEquals(data, expected, false) || partialDeepEquals(data, expected2) || partialDeepEquals(data, expected3)); + assert.strictEqual(data[0].segments.length, 2); + done(); + }) + .catch(err => done(err)); + }); }); diff --git a/test/cases/postSkipSegments.ts b/test/cases/postSkipSegments.ts index ec16dbd..6d849d7 100644 --- a/test/cases/postSkipSegments.ts +++ b/test/cases/postSkipSegments.ts @@ -37,6 +37,7 @@ describe("postSkipSegments", () => { 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 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 queryDatabaseVideoInfo = (videoID: string) => db.prepare("get", `SELECT * FROM "videoInfo" WHERE "videoID" = ?`, [videoID]); @@ -181,6 +182,34 @@ describe("postSkipSegments", () => { .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 music_offtopic with mute action type (JSON method)", (done) => { const videoID = "postSkip4"; postSkipSegmentJSON({ @@ -201,6 +230,46 @@ describe("postSkipSegments", () => { .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) => { const videoID = "postSkip5"; postSkipSegmentJSON({ diff --git a/test/utils/partialDeepEquals.ts b/test/utils/partialDeepEquals.ts index 200252a..df651d6 100644 --- a/test/utils/partialDeepEquals.ts +++ b/test/utils/partialDeepEquals.ts @@ -14,8 +14,7 @@ export const partialDeepEquals = (actual: Record, expected: Record< if (print) printActualExpected(actual, expected); return false; } - } - else if (actual?.[key] !== value) { + } else if (actual?.[key] !== value) { if (print) printActualExpected(actual, expected); return false; }