Merge pull request #401 from ajayyy/chapters

Initial Chapters
This commit is contained in:
Ajay Ramachandran
2021-11-14 16:41:51 -05:00
committed by GitHub
18 changed files with 440 additions and 48 deletions

View File

@@ -44,6 +44,7 @@
| shadowHidden | INTEGER | not null | | shadowHidden | INTEGER | not null |
| hashedVideoID | TEXT | not null, default '', sha256 | | hashedVideoID | TEXT | not null, default '', sha256 |
| userAgent | TEXT | not null, default '' | | userAgent | TEXT | not null, default '' |
| description | TEXT | not null, default '' |
| index | field | | index | field |
| -- | :--: | | -- | :--: |

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

@@ -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) ("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; 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 -- userNames
CREATE INDEX IF NOT EXISTS "userNames_userID" CREATE INDEX IF NOT EXISTS "userNames_userID"

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

@@ -42,6 +42,7 @@ import { getUserStats } from "./routes/getUserStats";
import ExpressPromiseRouter from "express-promise-router"; import ExpressPromiseRouter from "express-promise-router";
import { Server } from "http"; import { Server } from "http";
import { youtubeApiProxy } from "./routes/youtubeApiProxy"; import { youtubeApiProxy } from "./routes/youtubeApiProxy";
import { getChapterNames } from "./routes/getChapterNames";
export function createServer(callback: () => void): Server { export function createServer(callback: () => void): Server {
// Create a service (the app object is just a callback). // 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 // get all segments that match a search
router.get("/api/searchSegments", getSearchSegments); router.get("/api/searchSegments", getSearchSegments);
// autocomplete chapter names
router.get("/api/chapterNames", getChapterNames);
// get status // get status
router.get("/api/status/:value", getStatus); router.get("/api/status/:value", getStatus);
router.get("/api/status", getStatus); router.get("/api/status", getStatus);

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", "filler", "poi_highlight"], categoryList: ["sponsor", "selfpromo", "interaction", "intro", "outro", "preview", "music_offtopic", "filler", "poi_highlight", "chapter"],
categorySupport: { categorySupport: {
sponsor: ["skip", "mute"], sponsor: ["skip", "mute"],
selfpromo: ["skip", "mute"], selfpromo: ["skip", "mute"],
@@ -30,6 +30,7 @@ addDefaults(config, {
filler: ["skip", "mute"], filler: ["skip", "mute"],
music_offtopic: ["skip"], music_offtopic: ["skip"],
poi_highlight: ["skip"], poi_highlight: ["skip"],
chapter: ["chapter"]
}, },
maxNumberOfActiveWarnings: 1, maxNumberOfActiveWarnings: 1,
hoursAfterWarningExpires: 24, hoursAfterWarningExpires: 24,

View File

@@ -68,7 +68,7 @@ export class Postgres implements IDatabase {
} }
case "all": { case "all": {
const values = queryResult.rows; const values = queryResult.rows;
Logger.debug(`result (postgres): ${values}`); Logger.debug(`result (postgres): ${JSON.stringify(values)}`);
return values; return values;
} }
case "run": { case "run": {

View File

@@ -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<Response> {
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([]);
}

View File

@@ -45,7 +45,7 @@ async function prepareCategorySegments(req: Request, videoID: VideoID, category:
const filteredSegments = segments.filter((_, index) => shouldFilter[index]); 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) => ({ return (await chooseSegments(filteredSegments, maxSegments)).map((chosenSegment) => ({
category: chosenSegment.category, category: chosenSegment.category,
actionType: chosenSegment.actionType, actionType: chosenSegment.actionType,
@@ -55,6 +55,7 @@ async function prepareCategorySegments(req: Request, videoID: VideoID, category:
votes: chosenSegment.votes, votes: chosenSegment.votes,
videoDuration: chosenSegment.videoDuration, videoDuration: chosenSegment.videoDuration,
userID: chosenSegment.userID, userID: chosenSegment.userID,
description: chosenSegment.description
})); }));
} }
@@ -139,7 +140,7 @@ async function getSegmentsFromDBByHash(hashedVideoIDPrefix: VideoIDHash, service
const fetchFromDB = () => db const fetchFromDB = () => db
.prepare( .prepare(
"all", "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"`, WHERE "hashedVideoID" LIKE ? AND "service" = ? AND "hidden" = 0 ORDER BY "startTime"`,
[`${hashedVideoIDPrefix}%`, service] [`${hashedVideoIDPrefix}%`, service]
) as Promise<DBSegment[]>; ) as Promise<DBSegment[]>;
@@ -155,7 +156,7 @@ async function getSegmentsFromDBByVideoID(videoID: VideoID, service: Service): P
const fetchFromDB = () => db const fetchFromDB = () => db
.prepare( .prepare(
"all", "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"`, WHERE "videoID" = ? AND "service" = ? AND "hidden" = 0 ORDER BY "startTime"`,
[videoID, service] [videoID, service]
) as Promise<DBSegment[]>; ) as Promise<DBSegment[]>;
@@ -219,7 +220,7 @@ async function chooseSegments(segments: DBSegment[], max: number): Promise<DBSeg
//1. As long as the segments' startTime fall inside the currentGroup, we keep adding them to that group //1. As long as the segments' startTime fall inside the currentGroup, we keep adding them to that group
//2. If a segment starts after the end of the currentGroup (> cursor), no other segment will ever fall //2. If a segment starts after the end of the currentGroup (> cursor), no other segment will ever fall
// inside that group (because they're sorted) so we can create a new one // inside that group (because they're sorted) so we can create a new one
const overlappingSegmentsGroups: OverlappingSegmentGroup[] = []; let overlappingSegmentsGroups: OverlappingSegmentGroup[] = [];
let currentGroup: 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 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) { for (const segment of segments) {
@@ -261,6 +262,8 @@ async function chooseSegments(segments: DBSegment[], max: number): Promise<DBSeg
group.reputation = group.reputation / group.segments.length; group.reputation = group.reputation / group.segments.length;
}); });
overlappingSegmentsGroups = splitPercentOverlap(overlappingSegmentsGroups);
//if there are too many groups, find the best ones //if there are too many groups, find the best ones
return getWeightedRandomChoice(overlappingSegmentsGroups, max).map( return getWeightedRandomChoice(overlappingSegmentsGroups, max).map(
//randomly choose 1 good segment per group and return them //randomly choose 1 good segment per group and return them
@@ -268,6 +271,37 @@ async function chooseSegments(segments: DBSegment[], max: number): Promise<DBSeg
); );
} }
function splitPercentOverlap(groups: OverlappingSegmentGroup[]): OverlappingSegmentGroup[] {
return groups.flatMap((group) => {
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. * Returns what would be sent to the client.

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 {
@@ -65,6 +67,7 @@ export interface DBSegment {
timeSubmitted: number; timeSubmitted: number;
userAgent: string; userAgent: string;
service: Service; service: Service;
description: string;
} }
export interface OverlappingSegmentGroup { export interface OverlappingSegmentGroup {

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

@@ -0,0 +1,61 @@
import assert from "assert";
import { db } from "../../src/databases/databases";
import { Postgres } from "../../src/databases/Postgres";
import { client } from "../utils/httpClient";
import { partialDeepEquals } from "../utils/partialDeepEquals";
// Only works with Postgres
if (db instanceof Postgres) {
describe("getChapterNames", function () {
const endpoint = "/api/chapterNames";
const chapterNamesVid1 = "chapterNamesVid";
const chapterChannelID = "chapterChannelID";
before(async () => {
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));
});
});
}

View File

@@ -108,7 +108,9 @@ 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: "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); assert.deepStrictEqual(res.data, expected);
done(); done();

View File

@@ -6,23 +6,26 @@ import { client } from "../utils/httpClient";
describe("getSkipSegments", () => { describe("getSkipSegments", () => {
const endpoint = "/api/skipSegments"; const endpoint = "/api/skipSegments";
before(async () => { before(async () => {
const query = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "timeSubmitted", "views", "category", "actionType", "service", "videoDuration", "hidden", "shadowHidden") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; 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", 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", 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, ["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, ["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, ["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", 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, ["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", 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, ["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, 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, ["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, ["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, "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", 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, "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, ["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; return;
}); });
@@ -388,6 +391,33 @@ describe("getSkipSegments", () => {
.catch(err => done(err)); .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) => { it("Should get 400 if no videoID passed in", (done) => {
client.get(endpoint) client.get(endpoint)
.then(res => { .then(res => {

View File

@@ -16,20 +16,31 @@ describe("getSkipSegmentsByHash", () => {
const getSegmentsByHash0Hash = "fdaff4dee1043451faa7398324fb63d8618ebcd11bddfe0491c488db12c6c910"; const getSegmentsByHash0Hash = "fdaff4dee1043451faa7398324fb63d8618ebcd11bddfe0491c488db12c6c910";
const requiredSegmentVidHash = "d51822c3f681e07aef15a8855f52ad12db9eb9cf059e65b16b64c43359557f61"; const requiredSegmentVidHash = "d51822c3f681e07aef15a8855f52ad12db9eb9cf059e65b16b64c43359557f61";
before(async () => { before(async () => {
const query = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "UUID", "userID", "timeSubmitted", views, category, "actionType", "service", "hidden", "shadowHidden", "hashedVideoID") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; 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-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", 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", 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-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-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, ["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, ["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", 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, ["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-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", 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-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, ["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) => { it("Should be able to get a 200", (done) => {
@@ -356,4 +367,115 @@ describe("getSkipSegmentsByHash", () => {
}) })
.catch(err => done(err)); .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));
});
}); });

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 music_offtopic with mute action type (JSON method)", (done) => { it("Should not be able to submit an music_offtopic 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({

View File

@@ -14,8 +14,7 @@ export const partialDeepEquals = (actual: Record<string, any>, expected: Record<
if (print) printActualExpected(actual, expected); if (print) printActualExpected(actual, expected);
return false; return false;
} }
} } else if (actual?.[key] !== value) {
else if (actual?.[key] !== value) {
if (print) printActualExpected(actual, expected); if (print) printActualExpected(actual, expected);
return false; return false;
} }