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 |
| hashedVideoID | TEXT | not null, default '', sha256 |
| userAgent | TEXT | not null, default '' |
| description | TEXT | not null, default '' |
| index | field |
| -- | :--: |

View File

@@ -46,7 +46,6 @@
]
}
],
"categoryList": ["sponsor", "selfpromo", "interaction", "intro", "outro", "preview", "music_offtopic", "poi_highlight"],
"maxNumberOfActiveWarnings": 3,
"hoursAfterWarningExpires": 24,
"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)
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"

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 { 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);

View File

@@ -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,

View File

@@ -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": {

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 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<DBSegment[]>;
@@ -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<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
//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
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<DBSeg
group.reputation = group.reputation / group.segments.length;
});
overlappingSegmentsGroups = splitPercentOverlap(overlappingSegmentsGroups);
//if there are too many groups, find the best ones
return getWeightedRandomChoice(overlappingSegmentsGroups, max).map(
//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.

View File

@@ -299,7 +299,7 @@ async function checkUserActiveWarning(userID: string): Promise<CheckResult> {
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 errors = [];
if (typeof videoID !== "string") {
@@ -320,6 +320,12 @@ function checkInvalidFields(videoID: any, userID: any, segments: Array<any>): 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<Res
const startingLocked = isVIP ? 1 : 0;
try {
await db.prepare("run", `INSERT INTO "sponsorTimes"
("videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "timeSubmitted", "views", "category", "actionType", "service", "videoDuration", "reputation", "shadowHidden", "hashedVideoID", "userAgent")
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", "startTime", "endTime", "votes", "locked", "UUID", "userID", "timeSubmitted", "views", "category", "actionType", "service", "videoDuration", "reputation", "shadowHidden", "hashedVideoID", "userAgent", "description")
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, segmentInfo.description
],
);

View File

@@ -5,7 +5,7 @@ import { UserID } from "./user.model";
export type SegmentUUID = string & { __segmentUUIDBrand: unknown };
export type VideoID = string & { __videoIDBrand: 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 IPAddress = string & { __ipAddressBrand: unknown };
export type HashedIP = IPAddress & HashedValue;
@@ -13,6 +13,7 @@ export type HashedIP = IPAddress & HashedValue;
export enum ActionType {
Skip = "skip",
Mute = "mute",
Chapter = "chapter"
}
// Uncomment as needed
@@ -30,6 +31,7 @@ export interface IncomingSegment {
category: Category;
actionType: ActionType;
segment: string[];
description?: string;
}
export interface Segment {
@@ -65,6 +67,7 @@ export interface DBSegment {
timeSubmitted: number;
userAgent: string;
service: Service;
description: string;
}
export interface OverlappingSegmentGroup {

View File

@@ -42,7 +42,6 @@
]
}
],
"categoryList": ["sponsor", "selfpromo", "interaction", "intro", "outro", "preview", "music_offtopic", "poi_highlight"],
"maxNumberOfActiveWarnings": 3,
"hoursAfterWarningExpires": 24,
"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: "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();

View File

@@ -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 => {

View File

@@ -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));
});
});

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 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({

View File

@@ -14,8 +14,7 @@ export const partialDeepEquals = (actual: Record<string, any>, 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;
}