mirror of
https://github.com/ajayyy/SponsorBlockServer.git
synced 2024-11-10 01:02:30 +01:00
Merge branch 'master' into fix/general-fixes
This commit is contained in:
commit
4797a7d938
39 changed files with 1086 additions and 79 deletions
|
@ -28,6 +28,6 @@ module.exports = {
|
|||
"quotes": ["warn", "double", { "avoidEscape": true, "allowTemplateLiterals": true }],
|
||||
"require-await": "warn",
|
||||
"semi": "warn",
|
||||
"no-console": "error"
|
||||
"no-console": "warn"
|
||||
},
|
||||
};
|
||||
|
|
23
.github/workflows/generate-sqlite-base.yml
vendored
Normal file
23
.github/workflows/generate-sqlite-base.yml
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
name: create-sqlite-base
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- databases/**
|
||||
|
||||
jobs:
|
||||
make-base-db:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- run: npm install
|
||||
- name: Set config
|
||||
run: |
|
||||
echo '{"mode": "init-db-and-exit"}' > config.json
|
||||
- name: Run Server
|
||||
timeout-minutes: 10
|
||||
run: npm start
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: SponsorTimesDB.db
|
||||
path: databases/sponsorTimes.db
|
|
@ -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 |
|
||||
| -- | :--: |
|
||||
|
@ -184,6 +185,22 @@
|
|||
| hashedVideoID | TEXT | not null, default '', sha256 |
|
||||
| userAgent | TEXT | not null, default '' |
|
||||
|
||||
### ratings
|
||||
|
||||
| Name | Type | |
|
||||
| -- | :--: | -- |
|
||||
| videoID | TEXT | not null |
|
||||
| service | TEXT | not null, default 'YouTube' |
|
||||
| type | INTEGER | not null |
|
||||
| count | INTEGER | not null |
|
||||
| hashedVideoID | TEXT | not null |
|
||||
|
||||
| index | field |
|
||||
| -- | :--: |
|
||||
| ratings_hashedVideoID_gin | hashedVideoID |
|
||||
| ratings_hashedVideoID | hashedVideoID, service |
|
||||
| ratings_videoID | videoID, service |
|
||||
|
||||
# Private
|
||||
|
||||
[vote](#vote)
|
||||
|
@ -238,3 +255,18 @@
|
|||
| -- | :--: | -- |
|
||||
| key | TEXT | not null |
|
||||
| value | TEXT | not null |
|
||||
|
||||
### ratings
|
||||
|
||||
| Name | Type | |
|
||||
| -- | :--: | -- |
|
||||
| videoID | TEXT | not null |
|
||||
| service | TEXT | not null, default 'YouTube' |
|
||||
| userID | TEXT | not null |
|
||||
| type | INTEGER | not null |
|
||||
| timeSubmitted | INTEGER | not null |
|
||||
| hashedIP | TEXT | not null |
|
||||
|
||||
| index | field |
|
||||
| -- | :--: |
|
||||
| ratings_videoID | videoID, service, userID, timeSubmitted |
|
||||
|
|
1
ci.json
1
ci.json
|
@ -46,7 +46,6 @@
|
|||
]
|
||||
}
|
||||
],
|
||||
"categoryList": ["sponsor", "selfpromo", "interaction", "intro", "outro", "preview", "music_offtopic", "poi_highlight"],
|
||||
"maxNumberOfActiveWarnings": 3,
|
||||
"hoursAfterWarningExpires": 24,
|
||||
"rateLimit": {
|
||||
|
|
|
@ -23,3 +23,10 @@ CREATE INDEX IF NOT EXISTS "categoryVotes_UUID"
|
|||
ON public."categoryVotes" USING btree
|
||||
("UUID" COLLATE pg_catalog."default" ASC NULLS LAST, "userID" COLLATE pg_catalog."default" ASC NULLS LAST, "hashedIP" COLLATE pg_catalog."default" ASC NULLS LAST, category COLLATE pg_catalog."default" ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
-- ratings
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "ratings_videoID"
|
||||
ON public."ratings" USING btree
|
||||
("videoID" COLLATE pg_catalog."default" ASC NULLS LAST, service COLLATE pg_catalog."default" ASC NULLS LAST, "userID" COLLATE pg_catalog."default" ASC NULLS LAST, "timeSubmitted" ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
|
@ -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"
|
||||
|
@ -82,3 +87,20 @@ CREATE INDEX IF NOT EXISTS "videoInfo_channelID"
|
|||
ON public."videoInfo" USING btree
|
||||
("channelID" COLLATE pg_catalog."default" ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
-- ratings
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "ratings_hashedVideoID_gin"
|
||||
ON public."ratings" USING gin
|
||||
("hashedVideoID" COLLATE pg_catalog."default" gin_trgm_ops, category COLLATE pg_catalog."default" gin_trgm_ops)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "ratings_hashedVideoID"
|
||||
ON public."ratings" USING btree
|
||||
("hashedVideoID" COLLATE pg_catalog."default" ASC NULLS LAST, service COLLATE pg_catalog."default" ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "ratings_videoID"
|
||||
ON public."ratings" USING btree
|
||||
("videoID" COLLATE pg_catalog."default" ASC NULLS LAST, service COLLATE pg_catalog."default" ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
14
databases/_upgrade_private_4.sql
Normal file
14
databases/_upgrade_private_4.sql
Normal file
|
@ -0,0 +1,14 @@
|
|||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "ratings" (
|
||||
"videoID" TEXT NOT NULL,
|
||||
"service" TEXT NOT NULL default 'YouTube',
|
||||
"type" INTEGER NOT NULL,
|
||||
"userID" TEXT NOT NULL,
|
||||
"timeSubmitted" INTEGER NOT NULL,
|
||||
"hashedIP" TEXT NOT NULL
|
||||
);
|
||||
|
||||
UPDATE "config" SET value = 4 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
8
databases/_upgrade_sponsorTimes_27.sql
Normal file
8
databases/_upgrade_sponsorTimes_27.sql
Normal 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;
|
13
databases/_upgrade_sponsorTimes_28.sql
Normal file
13
databases/_upgrade_sponsorTimes_28.sql
Normal file
|
@ -0,0 +1,13 @@
|
|||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "ratings" (
|
||||
"videoID" TEXT NOT NULL,
|
||||
"service" TEXT NOT NULL default 'YouTube',
|
||||
"type" INTEGER NOT NULL,
|
||||
"count" INTEGER NOT NULL,
|
||||
"hashedVideoID" TEXT NOT NULL
|
||||
);
|
||||
|
||||
UPDATE "config" SET value = 28 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
14
src/app.ts
14
src/app.ts
|
@ -42,6 +42,10 @@ 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";
|
||||
import { postRating } from "./routes/ratings/postRating";
|
||||
import { getRating } from "./routes/ratings/getRating";
|
||||
import { postClearCache as ratingPostClearCache } from "./routes/ratings/postClearCache";
|
||||
|
||||
export function createServer(callback: () => void): Server {
|
||||
// Create a service (the app object is just a callback).
|
||||
|
@ -73,9 +77,11 @@ function setupRoutes(router: Router) {
|
|||
// Rate limit endpoint lists
|
||||
const voteEndpoints: RequestHandler[] = [voteOnSponsorTime];
|
||||
const viewEndpoints: RequestHandler[] = [viewedVideoSponsorTime];
|
||||
const postRateEndpoints: RequestHandler[] = [postRating];
|
||||
if (config.rateLimit) {
|
||||
if (config.rateLimit.vote) voteEndpoints.unshift(rateLimitMiddleware(config.rateLimit.vote, voteGetUserID));
|
||||
if (config.rateLimit.view) viewEndpoints.unshift(rateLimitMiddleware(config.rateLimit.view));
|
||||
if (config.rateLimit.rate) postRateEndpoints.unshift(rateLimitMiddleware(config.rateLimit.rate));
|
||||
}
|
||||
|
||||
//add the get function
|
||||
|
@ -172,6 +178,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);
|
||||
|
@ -182,6 +191,11 @@ function setupRoutes(router: Router) {
|
|||
|
||||
router.get("/api/lockReason", getLockReason);
|
||||
|
||||
// ratings
|
||||
router.get("/api/ratings/rate/:prefix", getRating);
|
||||
router.post("/api/ratings/rate", postRateEndpoints);
|
||||
router.post("/api/ratings/clearCache", ratingPostClearCache);
|
||||
|
||||
if (config.postgres) {
|
||||
router.get("/database", (req, res) => dumpDatabase(req, res, true));
|
||||
router.get("/database.json", (req, res) => dumpDatabase(req, res, false));
|
||||
|
|
|
@ -19,16 +19,18 @@ addDefaults(config, {
|
|||
privateDBSchema: "./databases/_private.db.sql",
|
||||
readOnly: false,
|
||||
webhooks: [],
|
||||
categoryList: ["sponsor", "selfpromo", "interaction", "intro", "outro", "preview", "music_offtopic", "poi_highlight"],
|
||||
categoryList: ["sponsor", "selfpromo", "interaction", "intro", "outro", "preview", "music_offtopic", "filler", "poi_highlight", "chapter"],
|
||||
categorySupport: {
|
||||
sponsor: ["skip", "mute"],
|
||||
selfpromo: ["skip", "mute"],
|
||||
interaction: ["skip", "mute"],
|
||||
intro: ["skip"],
|
||||
outro: ["skip"],
|
||||
preview: ["skip"],
|
||||
intro: ["skip", "mute"],
|
||||
outro: ["skip", "mute"],
|
||||
preview: ["skip", "mute"],
|
||||
filler: ["skip", "mute"],
|
||||
music_offtopic: ["skip"],
|
||||
poi_highlight: ["skip"],
|
||||
chapter: ["chapter"]
|
||||
},
|
||||
maxNumberOfActiveWarnings: 1,
|
||||
hoursAfterWarningExpires: 24,
|
||||
|
@ -56,6 +58,12 @@ addDefaults(config, {
|
|||
statusCode: 200,
|
||||
message: "Too many views, please try again later",
|
||||
},
|
||||
rate: {
|
||||
windowMs: 900000,
|
||||
max: 20,
|
||||
statusCode: 200,
|
||||
message: "Success",
|
||||
}
|
||||
},
|
||||
userCounterURL: null,
|
||||
newLeafURLs: null,
|
||||
|
|
|
@ -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": {
|
||||
|
|
46
src/routes/getChapterNames.ts
Normal file
46
src/routes/getChapterNames.ts
Normal 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([]);
|
||||
}
|
|
@ -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,
|
||||
|
@ -53,7 +53,9 @@ async function prepareCategorySegments(req: Request, videoID: VideoID, category:
|
|||
UUID: chosenSegment.UUID,
|
||||
locked: chosenSegment.locked,
|
||||
votes: chosenSegment.votes,
|
||||
videoDuration: chosenSegment.videoDuration
|
||||
videoDuration: chosenSegment.videoDuration,
|
||||
userID: chosenSegment.userID,
|
||||
description: chosenSegment.description
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -138,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[]>;
|
||||
|
@ -154,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[]>;
|
||||
|
@ -218,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) {
|
||||
|
@ -260,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
|
||||
|
@ -267,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.
|
||||
|
|
|
@ -39,7 +39,7 @@ export async function postClearCache(req: Request, res: Response): Promise<Respo
|
|||
}
|
||||
|
||||
try {
|
||||
QueryCacher.clearVideoCache({
|
||||
QueryCacher.clearSegmentCache({
|
||||
videoID,
|
||||
hashedVideoID,
|
||||
service
|
||||
|
|
|
@ -31,7 +31,7 @@ export async function postPurgeAllSegments(req: Request, res: Response): Promise
|
|||
await db.prepare("run", `UPDATE "sponsorTimes" SET "hidden" = 1 WHERE "videoID" = ?`, [videoID]);
|
||||
|
||||
const hashedVideoID: VideoIDHash = getHash(videoID, 1);
|
||||
QueryCacher.clearVideoCache({
|
||||
QueryCacher.clearSegmentCache({
|
||||
videoID,
|
||||
hashedVideoID,
|
||||
service
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
@ -620,7 +628,6 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
|
|||
|
||||
//check to see if this user is shadowbanned
|
||||
const shadowBanRow = await db.prepare("get", `SELECT count(*) as "userCount" FROM "shadowBannedUsers" WHERE "userID" = ? LIMIT 1`, [userID]);
|
||||
|
||||
const startingVotes = 0 + decreaseVotes;
|
||||
const reputation = await getReputation(userID);
|
||||
|
||||
|
@ -634,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, 0, 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
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -649,7 +657,7 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
|
|||
videoID, apiVideoInfo?.data?.authorId || "", apiVideoInfo?.data?.title || "", apiVideoInfo?.data?.published || 0, apiVideoInfo?.data?.genreUrl || "", videoID]);
|
||||
|
||||
// Clear redis cache for this video
|
||||
QueryCacher.clearVideoCache({
|
||||
QueryCacher.clearSegmentCache({
|
||||
videoID,
|
||||
hashedVideoID,
|
||||
service,
|
||||
|
|
75
src/routes/ratings/getRating.ts
Normal file
75
src/routes/ratings/getRating.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
import { Request, Response } from "express";
|
||||
import { db } from "../../databases/databases";
|
||||
import { RatingType } from "../../types/ratings.model";
|
||||
import { Service, VideoID, VideoIDHash } from "../../types/segments.model";
|
||||
import { getService } from "../../utils/getService";
|
||||
import { hashPrefixTester } from "../../utils/hashPrefixTester";
|
||||
import { Logger } from "../../utils/logger";
|
||||
import { QueryCacher } from "../../utils/queryCacher";
|
||||
import { ratingHashKey } from "../../utils/redisKeys";
|
||||
|
||||
interface DBRating {
|
||||
videoID: VideoID,
|
||||
hashedVideoID: VideoIDHash,
|
||||
service: Service,
|
||||
type: RatingType,
|
||||
count: number
|
||||
}
|
||||
|
||||
export async function getRating(req: Request, res: Response): Promise<Response> {
|
||||
let hashPrefix = req.params.prefix as VideoIDHash;
|
||||
if (!hashPrefix || !hashPrefixTester(hashPrefix)) {
|
||||
return res.status(400).send("Hash prefix does not match format requirements."); // Exit early on faulty prefix
|
||||
}
|
||||
hashPrefix = hashPrefix.toLowerCase() as VideoIDHash;
|
||||
|
||||
let types: RatingType[] = [];
|
||||
try {
|
||||
types = req.query.types
|
||||
? JSON.parse(req.query.types as string)
|
||||
: req.query.type
|
||||
? Array.isArray(req.query.type)
|
||||
? req.query.type
|
||||
: [req.query.type]
|
||||
: [RatingType.Upvote, RatingType.Downvote];
|
||||
if (!Array.isArray(types)) {
|
||||
return res.status(400).send("Types parameter does not match format requirements.");
|
||||
}
|
||||
|
||||
types = types.map((type) => parseInt(type as unknown as string, 10));
|
||||
} catch(error) {
|
||||
return res.status(400).send("Bad parameter: types (invalid JSON)");
|
||||
}
|
||||
|
||||
const service: Service = getService(req.query.service, req.body.service);
|
||||
|
||||
try {
|
||||
const ratings = (await getRatings(hashPrefix, service))
|
||||
.filter((rating) => types.includes(rating.type))
|
||||
.map((rating) => ({
|
||||
videoID: rating.videoID,
|
||||
hash: rating.hashedVideoID,
|
||||
service: rating.service,
|
||||
type: rating.type,
|
||||
count: rating.count
|
||||
}));
|
||||
return res.status((ratings.length) ? 200 : 404)
|
||||
.send(ratings ?? []);
|
||||
} catch (err) {
|
||||
Logger.error(err as string);
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
}
|
||||
|
||||
function getRatings(hashPrefix: VideoIDHash, service: Service): Promise<DBRating[]> {
|
||||
const fetchFromDB = () => db
|
||||
.prepare(
|
||||
"all",
|
||||
`SELECT "videoID", "hashedVideoID", "type", "count" FROM "ratings" WHERE "hashedVideoID" LIKE ? AND "service" = ? ORDER BY "hashedVideoID"`,
|
||||
[`${hashPrefix}%`, service]
|
||||
) as Promise<DBRating[]>;
|
||||
|
||||
return (hashPrefix.length === 4)
|
||||
? QueryCacher.get(fetchFromDB, ratingHashKey(hashPrefix, service))
|
||||
: fetchFromDB();
|
||||
}
|
52
src/routes/ratings/postClearCache.ts
Normal file
52
src/routes/ratings/postClearCache.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { Logger } from "../../utils/logger";
|
||||
import { HashedUserID, UserID } from "../../types/user.model";
|
||||
import { getHash } from "../../utils/getHash";
|
||||
import { Request, Response } from "express";
|
||||
import { Service, VideoID } from "../../types/segments.model";
|
||||
import { QueryCacher } from "../../utils/queryCacher";
|
||||
import { isUserVIP } from "../../utils/isUserVIP";
|
||||
import { VideoIDHash } from "../../types/segments.model";
|
||||
import { getService } from "../..//utils/getService";
|
||||
|
||||
export async function postClearCache(req: Request, res: Response): Promise<Response> {
|
||||
const videoID = req.query.videoID as VideoID;
|
||||
const userID = req.query.userID as UserID;
|
||||
const service = getService(req.query.service as Service);
|
||||
|
||||
const invalidFields = [];
|
||||
if (typeof videoID !== "string") {
|
||||
invalidFields.push("videoID");
|
||||
}
|
||||
if (typeof userID !== "string") {
|
||||
invalidFields.push("userID");
|
||||
}
|
||||
|
||||
if (invalidFields.length !== 0) {
|
||||
// invalid request
|
||||
const fields = invalidFields.reduce((p, c, i) => p + (i !== 0 ? ", " : "") + c, "");
|
||||
return res.status(400).send(`No valid ${fields} field(s) provided`);
|
||||
}
|
||||
|
||||
// hash the userID as early as possible
|
||||
const hashedUserID: HashedUserID = getHash(userID);
|
||||
// hash videoID
|
||||
const hashedVideoID: VideoIDHash = getHash(videoID, 1);
|
||||
|
||||
// Ensure user is a VIP
|
||||
if (!(await isUserVIP(hashedUserID))){
|
||||
Logger.warn(`Permission violation: User ${hashedUserID} attempted to clear cache for video ${videoID}.`);
|
||||
return res.status(403).json({ "message": "Not a VIP" });
|
||||
}
|
||||
|
||||
try {
|
||||
QueryCacher.clearRatingCache({
|
||||
hashedVideoID,
|
||||
service
|
||||
});
|
||||
return res.status(200).json({
|
||||
message: `Cache cleared on video ${videoID}`
|
||||
});
|
||||
} catch(err) {
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
}
|
63
src/routes/ratings/postRating.ts
Normal file
63
src/routes/ratings/postRating.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
import { db, privateDB } from "../../databases/databases";
|
||||
import { getHash } from "../../utils/getHash";
|
||||
import { Logger } from "../../utils/logger";
|
||||
import { Request, Response } from "express";
|
||||
import { HashedUserID, UserID } from "../../types/user.model";
|
||||
import { HashedIP, IPAddress, VideoID } from "../../types/segments.model";
|
||||
import { getIP } from "../../utils/getIP";
|
||||
import { getService } from "../../utils/getService";
|
||||
import { RatingType, RatingTypes } from "../../types/ratings.model";
|
||||
import { config } from "../../config";
|
||||
import { QueryCacher } from "../../utils/queryCacher";
|
||||
|
||||
export async function postRating(req: Request, res: Response): Promise<Response> {
|
||||
const privateUserID = req.body.userID as UserID;
|
||||
const videoID = req.body.videoID as VideoID;
|
||||
const service = getService(req.query.service, req.body.service);
|
||||
const type = req.body.type as RatingType;
|
||||
const enabled = req.body.enabled ?? true;
|
||||
|
||||
if (privateUserID == undefined || videoID == undefined || service == undefined || type == undefined
|
||||
|| (typeof privateUserID !== "string") || (typeof videoID !== "string") || (typeof service !== "string")
|
||||
|| (typeof type !== "number") || (enabled && (typeof enabled !== "boolean")) || !RatingTypes.includes(type)) {
|
||||
//invalid request
|
||||
return res.sendStatus(400);
|
||||
}
|
||||
|
||||
const hashedIP: HashedIP = getHash(getIP(req) + config.globalSalt as IPAddress, 1);
|
||||
const hashedUserID: HashedUserID = getHash(privateUserID);
|
||||
const hashedVideoID = getHash(videoID, 1);
|
||||
|
||||
try {
|
||||
// Check if this user has voted before
|
||||
const existingVote = await privateDB.prepare("get", `SELECT count(*) as "count" FROM "ratings" WHERE "videoID" = ? AND "service" = ? AND "type" = ? AND "userID" = ?`, [videoID, service, type, hashedUserID]);
|
||||
if (existingVote.count > 0 && !enabled) {
|
||||
// Undo the vote
|
||||
await db.prepare("run", `UPDATE "ratings" SET "count" = "count" - 1 WHERE "videoID" = ? AND "service" = ? AND type = ?`, [videoID, service, type]);
|
||||
await privateDB.prepare("run", `DELETE FROM "ratings" WHERE "videoID" = ? AND "service" = ? AND "type" = ? AND "userID" = ?`, [videoID, service, type, hashedUserID]);
|
||||
} else if (existingVote.count === 0 && enabled) {
|
||||
// Make sure there hasn't been another vote from this IP
|
||||
const existingIPVote = (await privateDB.prepare("get", `SELECT count(*) as "count" FROM "ratings" WHERE "videoID" = ? AND "service" = ? AND "type" = ? AND "hashedIP" = ?`, [videoID, service, type, hashedIP]))
|
||||
.count > 0;
|
||||
if (existingIPVote) { // if exisiting vote, exit early instead
|
||||
return res.sendStatus(200);
|
||||
}
|
||||
// Check if general rating already exists, if so increase it
|
||||
const rating = await db.prepare("get", `SELECT count(*) as "count" FROM "ratings" WHERE "videoID" = ? AND "service" = ? AND type = ?`, [videoID, service, type]);
|
||||
if (rating.count > 0) {
|
||||
await db.prepare("run", `UPDATE "ratings" SET "count" = "count" + 1 WHERE "videoID" = ? AND "service" = ? AND type = ?`, [videoID, service, type]);
|
||||
} else {
|
||||
await db.prepare("run", `INSERT INTO "ratings" ("videoID", "service", "type", "count", "hashedVideoID") VALUES (?, ?, ?, 1, ?)`, [videoID, service, type, hashedVideoID]);
|
||||
}
|
||||
|
||||
// Create entry in privateDB
|
||||
await privateDB.prepare("run", `INSERT INTO "ratings" ("videoID", "service", "type", "userID", "timeSubmitted", "hashedIP") VALUES (?, ?, ?, ?, ?, ?)`, [videoID, service, type, hashedUserID, Date.now(), hashedIP]);
|
||||
}
|
||||
// clear rating cache
|
||||
QueryCacher.clearRatingCache({ hashedVideoID, service });
|
||||
return res.sendStatus(200);
|
||||
} catch (err) {
|
||||
Logger.error(err as string);
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
}
|
|
@ -68,7 +68,7 @@ export async function shadowBanUser(req: Request, res: Response): Promise<Respon
|
|||
// collect list for unshadowbanning
|
||||
(await db.prepare("all", `SELECT "videoID", "hashedVideoID", "service", "votes", "views", "userID" FROM "sponsorTimes" WHERE "UUID" = ? AND "shadowHidden" = 1 AND "category" in (${categories.map((c) => `'${c}'`).join(",")})`, [UUID]))
|
||||
.forEach((videoInfo: {category: Category, videoID: VideoID, hashedVideoID: VideoIDHash, service: Service, userID: UserID}) => {
|
||||
QueryCacher.clearVideoCache(videoInfo);
|
||||
QueryCacher.clearSegmentCache(videoInfo);
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -125,6 +125,6 @@ async function unHideSubmissions(categories: string[], userID: UserID) {
|
|||
// clear cache for all old videos
|
||||
(await db.prepare("all", `SELECT "videoID", "hashedVideoID", "service", "votes", "views" FROM "sponsorTimes" WHERE "userID" = ?`, [userID]))
|
||||
.forEach((videoInfo: { category: Category; videoID: VideoID; hashedVideoID: VideoIDHash; service: Service; userID: UserID; }) => {
|
||||
QueryCacher.clearVideoCache(videoInfo);
|
||||
QueryCacher.clearSegmentCache(videoInfo);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -267,7 +267,7 @@ async function categoryVote(UUID: SegmentUUID, userID: UserID, isVIP: boolean, i
|
|||
}
|
||||
}
|
||||
|
||||
QueryCacher.clearVideoCache(videoInfo);
|
||||
QueryCacher.clearSegmentCache(videoInfo);
|
||||
|
||||
return res.sendStatus(finalResponse.finalStatus);
|
||||
}
|
||||
|
@ -473,7 +473,7 @@ export async function voteOnSponsorTime(req: Request, res: Response): Promise<Re
|
|||
await db.prepare("run", 'UPDATE "sponsorTimes" SET locked = 0 WHERE "UUID" = ?', [UUID]);
|
||||
}
|
||||
|
||||
QueryCacher.clearVideoCache(videoInfo);
|
||||
QueryCacher.clearSegmentCache(videoInfo);
|
||||
}
|
||||
if (incrementAmount - oldIncrementAmount !== 0) {
|
||||
sendWebhooks({
|
||||
|
|
|
@ -34,6 +34,7 @@ export interface SBSConfig {
|
|||
rateLimit: {
|
||||
vote: RateLimitConfig;
|
||||
view: RateLimitConfig;
|
||||
rate: RateLimitConfig;
|
||||
};
|
||||
mysql?: any;
|
||||
privateMysql?: any;
|
||||
|
|
6
src/types/ratings.model.ts
Normal file
6
src/types/ratings.model.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export enum RatingType {
|
||||
Downvote = 0,
|
||||
Upvote = 1
|
||||
}
|
||||
|
||||
export const RatingTypes = [RatingType.Downvote, RatingType.Upvote];
|
|
@ -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 {
|
||||
|
|
|
@ -15,6 +15,6 @@ export function getIP(req: Request): IPAddress {
|
|||
case "X-Real-IP":
|
||||
return req.headers["x-real-ip"] as IPAddress;
|
||||
default:
|
||||
return req.connection.remoteAddress as IPAddress;
|
||||
return (req.connection?.remoteAddress || req.socket?.remoteAddress) as IPAddress;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import redis from "../utils/redis";
|
||||
import { Logger } from "../utils/logger";
|
||||
import { skipSegmentsHashKey, skipSegmentsKey, reputationKey } from "./redisKeys";
|
||||
import { skipSegmentsHashKey, skipSegmentsKey, reputationKey, ratingHashKey } from "./redisKeys";
|
||||
import { Service, VideoID, VideoIDHash } from "../types/segments.model";
|
||||
import { UserID } from "../types/user.model";
|
||||
|
||||
|
@ -22,7 +22,7 @@ async function get<T>(fetchFromDB: () => Promise<T>, key: string): Promise<T> {
|
|||
return data;
|
||||
}
|
||||
|
||||
function clearVideoCache(videoInfo: { videoID: VideoID; hashedVideoID: VideoIDHash; service: Service; userID?: UserID; }): void {
|
||||
function clearSegmentCache(videoInfo: { videoID: VideoID; hashedVideoID: VideoIDHash; service: Service; userID?: UserID; }): void {
|
||||
if (videoInfo) {
|
||||
redis.delAsync(skipSegmentsKey(videoInfo.videoID, videoInfo.service));
|
||||
redis.delAsync(skipSegmentsHashKey(videoInfo.hashedVideoID, videoInfo.service));
|
||||
|
@ -30,7 +30,14 @@ function clearVideoCache(videoInfo: { videoID: VideoID; hashedVideoID: VideoIDHa
|
|||
}
|
||||
}
|
||||
|
||||
function clearRatingCache(videoInfo: { hashedVideoID: VideoIDHash; service: Service;}): void {
|
||||
if (videoInfo) {
|
||||
redis.delAsync(ratingHashKey(videoInfo.hashedVideoID, videoInfo.service));
|
||||
}
|
||||
}
|
||||
|
||||
export const QueryCacher = {
|
||||
get,
|
||||
clearVideoCache
|
||||
clearSegmentCache,
|
||||
clearRatingCache
|
||||
};
|
|
@ -16,3 +16,10 @@ export function skipSegmentsHashKey(hashedVideoIDPrefix: VideoIDHash, service: S
|
|||
export function reputationKey(userID: UserID): string {
|
||||
return `reputation.user.${userID}`;
|
||||
}
|
||||
|
||||
export function ratingHashKey(hashPrefix: VideoIDHash, service: Service): string {
|
||||
hashPrefix = hashPrefix.substring(0, 4) as VideoIDHash;
|
||||
if (hashPrefix.length !== 4) Logger.warn(`Redis rating hash-prefix key is not length 4! ${hashPrefix}`);
|
||||
|
||||
return `rating.${service}.${hashPrefix}`;
|
||||
}
|
|
@ -42,7 +42,6 @@
|
|||
]
|
||||
}
|
||||
],
|
||||
"categoryList": ["sponsor", "selfpromo", "interaction", "intro", "outro", "preview", "music_offtopic", "poi_highlight"],
|
||||
"maxNumberOfActiveWarnings": 3,
|
||||
"hoursAfterWarningExpires": 24,
|
||||
"rateLimit": {
|
||||
|
|
61
test/cases/getChapterNames.ts
Normal file
61
test/cases/getChapterNames.ts
Normal 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));
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
||||
|
@ -39,6 +42,7 @@ describe("getSkipSegments", () => {
|
|||
assert.strictEqual(data[0].votes, 1);
|
||||
assert.strictEqual(data[0].locked, 0);
|
||||
assert.strictEqual(data[0].videoDuration, 100);
|
||||
assert.strictEqual(data[0].userID, "testman");
|
||||
done();
|
||||
})
|
||||
.catch(err => done(err));
|
||||
|
@ -387,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 => {
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -26,14 +26,18 @@ describe("postSkipSegments", () => {
|
|||
const warnUser03Hash = getHash(warnUser03);
|
||||
const warnUser04 = "warn-user04-qwertyuiopasdfghjklzxcvbnm";
|
||||
const warnUser04Hash = getHash(warnUser04);
|
||||
const banUser01 = "ban-user01-loremipsumdolorsitametconsectetur";
|
||||
const banUser01Hash = getHash(banUser01);
|
||||
|
||||
const submitUserOneHash = getHash(submitUserOne);
|
||||
const submitVIPuser = `VIPPostSkipUser${".".repeat(16)}`;
|
||||
const warnVideoID = "postSkip2";
|
||||
const badInputVideoID = "dQw4w9WgXcQ";
|
||||
const shadowBanVideoID = "postSkipBan";
|
||||
|
||||
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]);
|
||||
|
||||
|
@ -88,6 +92,9 @@ describe("postSkipSegments", () => {
|
|||
|
||||
const insertVipUserQuery = 'INSERT INTO "vipUsers" ("userID") VALUES (?)';
|
||||
db.prepare("run", insertVipUserQuery, [getHash(submitVIPuser)]);
|
||||
|
||||
// ban user
|
||||
db.prepare("run", `INSERT INTO "shadowBannedUsers" ("userID") VALUES(?)`, [banUser01Hash]);
|
||||
});
|
||||
|
||||
it("Should be able to submit a single time (Params method)", (done) => {
|
||||
|
@ -175,14 +182,42 @@ describe("postSkipSegments", () => {
|
|||
.catch(err => done(err));
|
||||
});
|
||||
|
||||
it("Should not be able to submit an intro with mute action type (JSON method)", (done) => {
|
||||
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({
|
||||
userID: submitUserOne,
|
||||
videoID,
|
||||
segments: [{
|
||||
segment: [0, 10],
|
||||
category: "intro",
|
||||
category: "music_offtopic",
|
||||
actionType: "mute"
|
||||
}],
|
||||
})
|
||||
|
@ -195,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({
|
||||
|
@ -985,4 +1060,28 @@ describe("postSkipSegments", () => {
|
|||
})
|
||||
.catch(err => done(err));
|
||||
});
|
||||
|
||||
it("Should automatically shadowban segments if user is banned", (done) => {
|
||||
const videoID = shadowBanVideoID;
|
||||
postSkipSegmentParam({
|
||||
videoID,
|
||||
startTime: 0,
|
||||
endTime: 10,
|
||||
category: "sponsor",
|
||||
userID: banUser01
|
||||
})
|
||||
.then(async res => {
|
||||
assert.strictEqual(res.status, 200);
|
||||
const row = await db.prepare("get", `SELECT "startTime", "endTime", "shadowHidden", "userID" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]);
|
||||
const expected = {
|
||||
startTime: 0,
|
||||
endTime: 10,
|
||||
shadowHidden: 1,
|
||||
userID: banUser01Hash
|
||||
};
|
||||
assert.deepStrictEqual(row, expected);
|
||||
done();
|
||||
})
|
||||
.catch(err => done(err));
|
||||
});
|
||||
});
|
||||
|
|
80
test/cases/ratings/getRating.ts
Normal file
80
test/cases/ratings/getRating.ts
Normal file
|
@ -0,0 +1,80 @@
|
|||
import { db } from "../../../src/databases/databases";
|
||||
import { getHash } from "../../../src/utils/getHash";
|
||||
import assert from "assert";
|
||||
import { client } from "../../utils/httpClient";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { partialDeepEquals } from "../../utils/partialDeepEquals";
|
||||
|
||||
const endpoint = "/api/ratings/rate/";
|
||||
const getRating = (hash: string, params?: unknown): Promise<AxiosResponse> => client.get(endpoint + hash, { params });
|
||||
|
||||
const videoOneID = "some-likes-and-dislikes";
|
||||
const videoOneIDHash = getHash(videoOneID, 1);
|
||||
const videoOnePartialHash = videoOneIDHash.substr(0, 4);
|
||||
|
||||
describe("getRating", () => {
|
||||
before(async () => {
|
||||
const insertUserNameQuery = 'INSERT INTO "ratings" ("videoID", "service", "type", "count", "hashedVideoID") VALUES (?, ?, ?, ?, ?)';
|
||||
await db.prepare("run", insertUserNameQuery, [videoOneID, "YouTube", 0, 5, videoOneIDHash]);
|
||||
await db.prepare("run", insertUserNameQuery, [videoOneID, "YouTube", 1, 10, videoOneIDHash]);
|
||||
});
|
||||
|
||||
it("Should be able to get dislikes and likes by default", (done) => {
|
||||
getRating(videoOnePartialHash)
|
||||
.then(res => {
|
||||
assert.strictEqual(res.status, 200);
|
||||
const expected = [{
|
||||
type: 0,
|
||||
count: 5,
|
||||
}, {
|
||||
type: 1,
|
||||
count: 10,
|
||||
}];
|
||||
assert.ok(partialDeepEquals(res.data, expected));
|
||||
done();
|
||||
})
|
||||
.catch(err => done(err));
|
||||
});
|
||||
|
||||
it("Should be able to filter for only dislikes", (done) => {
|
||||
getRating(videoOnePartialHash, { type: 0 })
|
||||
.then(res => {
|
||||
assert.strictEqual(res.status, 200);
|
||||
const expected = [{
|
||||
type: 0,
|
||||
count: 5,
|
||||
}];
|
||||
assert.ok(partialDeepEquals(res.data, expected));
|
||||
|
||||
done();
|
||||
})
|
||||
.catch(err => done(err));
|
||||
});
|
||||
|
||||
it("Should return 400 for invalid hash", (done) => {
|
||||
getRating("a")
|
||||
.then(res => {
|
||||
assert.strictEqual(res.status, 400);
|
||||
done();
|
||||
})
|
||||
.catch(err => done(err));
|
||||
});
|
||||
|
||||
it("Should return 404 for nonexitent type", (done) => {
|
||||
getRating(videoOnePartialHash, { type: 100 })
|
||||
.then(res => {
|
||||
assert.strictEqual(res.status, 404);
|
||||
done();
|
||||
})
|
||||
.catch(err => done(err));
|
||||
});
|
||||
|
||||
it("Should return 404 for nonexistent videoID", (done) => {
|
||||
getRating("aaaa")
|
||||
.then(res => {
|
||||
assert.strictEqual(res.status, 404);
|
||||
done();
|
||||
})
|
||||
.catch(err => done(err));
|
||||
});
|
||||
});
|
51
test/cases/ratings/postClearCache.ts
Normal file
51
test/cases/ratings/postClearCache.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
import { db } from "../../../src/databases/databases";
|
||||
import { getHash } from "../../../src/utils/getHash";
|
||||
import assert from "assert";
|
||||
import { client } from "../../utils/httpClient";
|
||||
|
||||
const VIPUser = "clearCacheVIP";
|
||||
const regularUser = "regular-user";
|
||||
const endpoint = "/api/ratings/clearCache";
|
||||
const postClearCache = (userID: string, videoID: string) => client({ method: "post", url: endpoint, params: { userID, videoID } });
|
||||
|
||||
describe("ratings postClearCache", () => {
|
||||
before(async () => {
|
||||
await db.prepare("run", `INSERT INTO "vipUsers" ("userID") VALUES ('${getHash(VIPUser)}')`);
|
||||
});
|
||||
|
||||
it("Should be able to clear cache amy video", (done) => {
|
||||
postClearCache(VIPUser, "dne-video")
|
||||
.then(res => {
|
||||
assert.strictEqual(res.status, 200);
|
||||
done();
|
||||
})
|
||||
.catch(err => done(err));
|
||||
});
|
||||
|
||||
it("Should get 403 as non-vip", (done) => {
|
||||
postClearCache(regularUser, "clear-test")
|
||||
.then(res => {
|
||||
assert.strictEqual(res.status, 403);
|
||||
done();
|
||||
})
|
||||
.catch(err => done(err));
|
||||
});
|
||||
|
||||
it("Should give 400 with missing videoID", (done) => {
|
||||
client.post(endpoint, { params: { userID: VIPUser } })
|
||||
.then(res => {
|
||||
assert.strictEqual(res.status, 400);
|
||||
done();
|
||||
})
|
||||
.catch(err => done(err));
|
||||
});
|
||||
|
||||
it("Should give 400 with missing userID", (done) => {
|
||||
client.post(endpoint, { params: { videoID: "clear-test" } })
|
||||
.then(res => {
|
||||
assert.strictEqual(res.status, 400);
|
||||
done();
|
||||
})
|
||||
.catch(err => done(err));
|
||||
});
|
||||
});
|
118
test/cases/ratings/postRating.ts
Normal file
118
test/cases/ratings/postRating.ts
Normal file
|
@ -0,0 +1,118 @@
|
|||
import { db } from "../../../src/databases/databases";
|
||||
import { getHash } from "../../../src/utils/getHash";
|
||||
import assert from "assert";
|
||||
import { client } from "../../utils/httpClient";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { partialDeepEquals } from "../../utils/partialDeepEquals";
|
||||
|
||||
const endpoint = "/api/ratings/rate/";
|
||||
const postRating = (body: unknown): Promise<AxiosResponse> => client.post(endpoint, body);
|
||||
const queryDatabase = (videoID: string) => db.prepare("all", `SELECT * FROM "ratings" WHERE "videoID" = ?`, [videoID]);
|
||||
|
||||
const videoIDOne = "normal-video";
|
||||
const videoIDTwo = "multiple-rates";
|
||||
const ratingUserID = "rating-testman";
|
||||
|
||||
describe("postRating", () => {
|
||||
before(async () => {
|
||||
const insertUserNameQuery = 'INSERT INTO "ratings" ("videoID", "service", "type", "count", "hashedVideoID") VALUES (?, ?, ?, ?, ?)';
|
||||
await db.prepare("run", insertUserNameQuery, [videoIDTwo, "YouTube", 0, 3, getHash(videoIDTwo, 1)]);
|
||||
});
|
||||
|
||||
it("Should be able to vote on a video", (done) => {
|
||||
const videoID = videoIDOne;
|
||||
postRating({
|
||||
userID: ratingUserID,
|
||||
videoID,
|
||||
type: 0
|
||||
})
|
||||
.then(async res => {
|
||||
assert.strictEqual(res.status, 200);
|
||||
const expected = [{
|
||||
hashedVideoID: getHash(videoID, 1),
|
||||
videoID,
|
||||
type: 0,
|
||||
count: 1,
|
||||
service: "YouTube"
|
||||
}];
|
||||
assert.ok(partialDeepEquals(await queryDatabase(videoID), expected));
|
||||
done();
|
||||
})
|
||||
.catch(err => done(err));
|
||||
});
|
||||
|
||||
it("Should be able to undo a vote on a video", (done) => {
|
||||
const videoID = videoIDOne;
|
||||
postRating({
|
||||
userID: ratingUserID,
|
||||
videoID,
|
||||
type: 0,
|
||||
enabled: false
|
||||
})
|
||||
.then(async res => {
|
||||
assert.strictEqual(res.status, 200);
|
||||
const expected = [{
|
||||
type: 0,
|
||||
count: 0
|
||||
}];
|
||||
assert.ok(partialDeepEquals(await queryDatabase(videoID), expected));
|
||||
done();
|
||||
})
|
||||
.catch(err => done(err));
|
||||
});
|
||||
|
||||
it("Should be able to vote after someone else on a video", (done) => {
|
||||
const videoID = videoIDTwo;
|
||||
postRating({
|
||||
userID: ratingUserID,
|
||||
videoID,
|
||||
type: 0
|
||||
})
|
||||
.then(async res => {
|
||||
assert.strictEqual(res.status, 200);
|
||||
const expected = [{
|
||||
type: 0,
|
||||
count: 4
|
||||
}];
|
||||
assert.ok(partialDeepEquals(await queryDatabase(videoID), expected));
|
||||
done();
|
||||
})
|
||||
.catch(err => done(err));
|
||||
});
|
||||
|
||||
it("Should be able to vote a different type than existing votes on a video", (done) => {
|
||||
const videoID = videoIDTwo;
|
||||
postRating({
|
||||
userID: ratingUserID,
|
||||
videoID,
|
||||
type: 1
|
||||
})
|
||||
.then(async res => {
|
||||
assert.strictEqual(res.status, 200);
|
||||
const expected = [{
|
||||
type: 0,
|
||||
count: 4
|
||||
}, {
|
||||
type: 1,
|
||||
count: 1
|
||||
}];
|
||||
assert.ok(partialDeepEquals(await queryDatabase(videoID), expected));
|
||||
done();
|
||||
})
|
||||
.catch(err => done(err));
|
||||
});
|
||||
|
||||
it("Should not be able to vote with nonexistent type", (done) => {
|
||||
const videoID = videoIDOne;
|
||||
postRating({
|
||||
userID: ratingUserID,
|
||||
videoID,
|
||||
type: 100
|
||||
})
|
||||
.then(res => {
|
||||
assert.strictEqual(res.status, 400);
|
||||
done();
|
||||
})
|
||||
.catch(err => done(err));
|
||||
});
|
||||
});
|
|
@ -32,9 +32,10 @@ async function init() {
|
|||
// Instantiate a Mocha instance.
|
||||
const mocha = new Mocha();
|
||||
|
||||
const testDir = "./test/cases";
|
||||
const testDirs = ["./test/cases", "./test/cases/ratings"];
|
||||
|
||||
// Add each .ts file to the mocha instance
|
||||
testDirs.forEach(testDir => {
|
||||
fs.readdirSync(testDir)
|
||||
.filter((file) =>
|
||||
// Only keep the .ts files
|
||||
|
@ -45,6 +46,7 @@ async function init() {
|
|||
path.join(testDir, file)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const mockServer = createMockServer(() => {
|
||||
Logger.info("Started mock HTTP Server");
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue