mirror of
https://github.com/ajayyy/SponsorBlockServer.git
synced 2025-12-09 13:07:02 +03:00
endpoint + tests for getVideoLabels
This commit is contained in:
@@ -48,6 +48,8 @@ import { getRating } from "./routes/ratings/getRating";
|
|||||||
import { postClearCache as ratingPostClearCache } from "./routes/ratings/postClearCache";
|
import { postClearCache as ratingPostClearCache } from "./routes/ratings/postClearCache";
|
||||||
import { getTopCategoryUsers } from "./routes/getTopCategoryUsers";
|
import { getTopCategoryUsers } from "./routes/getTopCategoryUsers";
|
||||||
import { addUserAsTempVIP } from "./routes/addUserAsTempVIP";
|
import { addUserAsTempVIP } from "./routes/addUserAsTempVIP";
|
||||||
|
import { endpoint as getVideoLabels } from "./routes/getVideoLabel";
|
||||||
|
import { getVideoLabelsByHash } from "./routes/getVideoLabelByHash";
|
||||||
|
|
||||||
export function createServer(callback: () => void): Server {
|
export function createServer(callback: () => void): Server {
|
||||||
// Create a service (the app object is just a callback).
|
// Create a service (the app object is just a callback).
|
||||||
@@ -202,6 +204,10 @@ function setupRoutes(router: Router) {
|
|||||||
router.post("/api/ratings/rate", postRateEndpoints);
|
router.post("/api/ratings/rate", postRateEndpoints);
|
||||||
router.post("/api/ratings/clearCache", ratingPostClearCache);
|
router.post("/api/ratings/clearCache", ratingPostClearCache);
|
||||||
|
|
||||||
|
// labels
|
||||||
|
router.get("/api/videoLabels", getVideoLabels);
|
||||||
|
router.get("/api/videoLabels/:prefix", getVideoLabelsByHash);
|
||||||
|
|
||||||
if (config.postgres) {
|
if (config.postgres) {
|
||||||
router.get("/database", (req, res) => dumpDatabase(req, res, true));
|
router.get("/database", (req, res) => dumpDatabase(req, res, true));
|
||||||
router.get("/database.json", (req, res) => dumpDatabase(req, res, false));
|
router.get("/database.json", (req, res) => dumpDatabase(req, res, false));
|
||||||
|
|||||||
176
src/routes/getVideoLabel.ts
Normal file
176
src/routes/getVideoLabel.ts
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import { db } from "../databases/databases";
|
||||||
|
import { videoLabelsHashKey, videoLabelsKey } from "../utils/redisKeys";
|
||||||
|
import { SBRecord } from "../types/lib.model";
|
||||||
|
import { DBSegment, Segment, Service, VideoData, VideoID, VideoIDHash } from "../types/segments.model";
|
||||||
|
import { Logger } from "../utils/logger";
|
||||||
|
import { QueryCacher } from "../utils/queryCacher";
|
||||||
|
import { getService } from "../utils/getService";
|
||||||
|
|
||||||
|
function transformDBSegments(segments: DBSegment[]): Segment[] {
|
||||||
|
return segments.map((chosenSegment) => ({
|
||||||
|
category: chosenSegment.category,
|
||||||
|
actionType: chosenSegment.actionType,
|
||||||
|
segment: [chosenSegment.startTime, chosenSegment.endTime],
|
||||||
|
UUID: chosenSegment.UUID,
|
||||||
|
locked: chosenSegment.locked,
|
||||||
|
votes: chosenSegment.votes,
|
||||||
|
videoDuration: chosenSegment.videoDuration,
|
||||||
|
userID: chosenSegment.userID,
|
||||||
|
description: chosenSegment.description
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getLabelsByVideoID(videoID: VideoID, service: Service): Promise<Segment[]> {
|
||||||
|
try {
|
||||||
|
const segments: DBSegment[] = await getSegmentsFromDBByVideoID(videoID, service);
|
||||||
|
console.log("dbSegments", segments);
|
||||||
|
return chooseSegment(segments);
|
||||||
|
} catch (err) {
|
||||||
|
if (err) {
|
||||||
|
Logger.error(err as string);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getLabelsbyHash(hashedVideoIDPrefix: VideoIDHash, service: Service): Promise<SBRecord<VideoID, VideoData>> {
|
||||||
|
const segments: SBRecord<VideoID, VideoData> = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
type SegmentWithHashPerVideoID = SBRecord<VideoID, { hash: VideoIDHash, segments: DBSegment[] }>;
|
||||||
|
|
||||||
|
const segmentPerVideoID: SegmentWithHashPerVideoID = (await getSegmentsFromDBByHash(hashedVideoIDPrefix, service))
|
||||||
|
.reduce((acc: SegmentWithHashPerVideoID, segment: DBSegment) => {
|
||||||
|
acc[segment.videoID] = acc[segment.videoID] || {
|
||||||
|
hash: segment.hashedVideoID,
|
||||||
|
segments: []
|
||||||
|
};
|
||||||
|
|
||||||
|
acc[segment.videoID].segments ??= [];
|
||||||
|
acc[segment.videoID].segments.push(segment);
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
for (const [videoID, videoData] of Object.entries(segmentPerVideoID)) {
|
||||||
|
const data: VideoData = {
|
||||||
|
hash: videoData.hash,
|
||||||
|
segments: chooseSegment(videoData.segments),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data.segments.length > 0) {
|
||||||
|
segments[videoID] = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments;
|
||||||
|
} catch (err) {
|
||||||
|
Logger.error(err as string);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSegmentsFromDBByHash(hashedVideoIDPrefix: VideoIDHash, service: Service): Promise<DBSegment[]> {
|
||||||
|
const fetchFromDB = () => db
|
||||||
|
.prepare(
|
||||||
|
"all",
|
||||||
|
`SELECT "startTime", "endTime", "videoID", "votes", "locked", "UUID", "userID", "category", "actionType", "hashedVideoID", "description" FROM "sponsorTimes"
|
||||||
|
WHERE "hashedVideoID" LIKE ? AND "service" = ? AND actionType = 'full' AND "hidden" = 0 AND "shadowHidden" = 0`,
|
||||||
|
[`${hashedVideoIDPrefix}%`, service]
|
||||||
|
) as Promise<DBSegment[]>;
|
||||||
|
|
||||||
|
if (hashedVideoIDPrefix.length === 4) {
|
||||||
|
return await QueryCacher.get(fetchFromDB, videoLabelsHashKey(hashedVideoIDPrefix, service));
|
||||||
|
}
|
||||||
|
|
||||||
|
return await fetchFromDB();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSegmentsFromDBByVideoID(videoID: VideoID, service: Service): Promise<DBSegment[]> {
|
||||||
|
const fetchFromDB = () => db
|
||||||
|
.prepare(
|
||||||
|
"all",
|
||||||
|
`SELECT "startTime", "endTime", "votes", "locked", "UUID", "userID", "category", "actionType", "description" FROM "sponsorTimes"
|
||||||
|
WHERE "videoID" = ? AND "service" = ? AND "hidden" = 0 AND "shadowHidden" = 0`,
|
||||||
|
[videoID, service]
|
||||||
|
) as Promise<DBSegment[]>;
|
||||||
|
|
||||||
|
return await QueryCacher.get(fetchFromDB, videoLabelsKey(videoID, service));
|
||||||
|
}
|
||||||
|
|
||||||
|
function chooseSegment<T extends DBSegment>(choices: T[]): Segment[] {
|
||||||
|
// filter out -2 segments
|
||||||
|
choices = choices.filter((segment) => segment.votes > -2);
|
||||||
|
const results = [];
|
||||||
|
// trivial decisions
|
||||||
|
if (choices.length === 0) {
|
||||||
|
return [];
|
||||||
|
} else if (choices.length === 1) {
|
||||||
|
return transformDBSegments(choices);
|
||||||
|
}
|
||||||
|
// if locked, only choose from locked
|
||||||
|
const locked = choices.filter((segment) => segment.locked);
|
||||||
|
if (locked.length > 0) {
|
||||||
|
choices = locked;
|
||||||
|
}
|
||||||
|
//no need to filter, just one label
|
||||||
|
if (choices.length === 1) {
|
||||||
|
return transformDBSegments(choices);
|
||||||
|
}
|
||||||
|
// sponsor > exclusive > selfpromo
|
||||||
|
const sponsorResult = choices.find((segment) => segment.category === "sponsor");
|
||||||
|
const eaResult = choices.find((segment) => segment.category === "exclusive_access");
|
||||||
|
const selfpromoResult = choices.find((segment) => segment.category === "selfpromo");
|
||||||
|
if (sponsorResult) {
|
||||||
|
results.push(sponsorResult);
|
||||||
|
} else if (eaResult) {
|
||||||
|
results.push(eaResult);
|
||||||
|
} else if (selfpromoResult) {
|
||||||
|
results.push(selfpromoResult);
|
||||||
|
}
|
||||||
|
console.log("chosenresults", results);
|
||||||
|
return transformDBSegments(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGetLabel(req: Request, res: Response): Promise<Segment[] | false> {
|
||||||
|
const videoID = req.query.videoID as VideoID;
|
||||||
|
if (!videoID) {
|
||||||
|
res.status(400).send("videoID not specified");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const service = getService(req.query.service, req.body.service);
|
||||||
|
const segments = await getLabelsByVideoID(videoID, service);
|
||||||
|
|
||||||
|
console.log("segments", segments);
|
||||||
|
|
||||||
|
if (!segments || segments.length === 0) {
|
||||||
|
res.sendStatus(404);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function endpoint(req: Request, res: Response): Promise<Response> {
|
||||||
|
try {
|
||||||
|
const segments = await handleGetLabel(req, res);
|
||||||
|
|
||||||
|
// If false, res.send has already been called
|
||||||
|
if (segments) {
|
||||||
|
//send result
|
||||||
|
return res.send(segments);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof SyntaxError) {
|
||||||
|
return res.status(400).send("Categories parameter does not match format requirements.");
|
||||||
|
} else return res.sendStatus(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
getLabelsByVideoID,
|
||||||
|
getLabelsbyHash,
|
||||||
|
endpoint
|
||||||
|
};
|
||||||
27
src/routes/getVideoLabelByHash.ts
Normal file
27
src/routes/getVideoLabelByHash.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { hashPrefixTester } from "../utils/hashPrefixTester";
|
||||||
|
import { getLabelsbyHash } from "./getVideoLabel";
|
||||||
|
import { Request, Response } from "express";
|
||||||
|
import { VideoIDHash, Service } from "../types/segments.model";
|
||||||
|
import { getService } from "../utils/getService";
|
||||||
|
|
||||||
|
export async function getVideoLabelsByHash(req: Request, res: Response): Promise<Response> {
|
||||||
|
let hashPrefix = req.params.prefix as VideoIDHash;
|
||||||
|
if (!req.params.prefix || !hashPrefixTester(req.params.prefix)) {
|
||||||
|
return res.status(400).send("Hash prefix does not match format requirements."); // Exit early on faulty prefix
|
||||||
|
}
|
||||||
|
hashPrefix = hashPrefix.toLowerCase() as VideoIDHash;
|
||||||
|
|
||||||
|
const service: Service = getService(req.query.service, req.body.service);
|
||||||
|
|
||||||
|
// Get all video id's that match hash prefix
|
||||||
|
const segments = await getLabelsbyHash(hashPrefix, service);
|
||||||
|
|
||||||
|
if (!segments) return res.status(404).json([]);
|
||||||
|
|
||||||
|
const output = Object.entries(segments).map(([videoID, data]) => ({
|
||||||
|
videoID,
|
||||||
|
hash: data.hash,
|
||||||
|
segments: data.segments,
|
||||||
|
}));
|
||||||
|
return res.status(output.length === 0 ? 404 : 200).json(output);
|
||||||
|
}
|
||||||
@@ -36,4 +36,14 @@ export function shaHashKey(singleIter: HashedValue): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const tempVIPKey = (userID: HashedUserID): string =>
|
export const tempVIPKey = (userID: HashedUserID): string =>
|
||||||
`vip.temp.${userID}`;
|
`vip.temp.${userID}`;
|
||||||
|
|
||||||
|
export const videoLabelsKey = (videoID: VideoID, service: Service): string =>
|
||||||
|
`labels.v1.${service}.videoID.${videoID}`;
|
||||||
|
|
||||||
|
export function videoLabelsHashKey(hashedVideoIDPrefix: VideoIDHash, service: Service): string {
|
||||||
|
hashedVideoIDPrefix = hashedVideoIDPrefix.substring(0, 4) as VideoIDHash;
|
||||||
|
if (hashedVideoIDPrefix.length !== 4) Logger.warn(`Redis skip segment hash-prefix key is not length 4! ${hashedVideoIDPrefix}`);
|
||||||
|
|
||||||
|
return `labels.v1.${service}.${hashedVideoIDPrefix}`;
|
||||||
|
}
|
||||||
170
test/cases/getVideoLabels.ts
Normal file
170
test/cases/getVideoLabels.ts
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import { db } from "../../src/databases/databases";
|
||||||
|
import assert from "assert";
|
||||||
|
import { client } from "../utils/httpClient";
|
||||||
|
|
||||||
|
describe("getVideoLabels", () => {
|
||||||
|
const endpoint = "/api/videoLabels";
|
||||||
|
before(async () => {
|
||||||
|
const query = 'INSERT INTO "sponsorTimes" ("videoID", "votes", "locked", "UUID", "userID", "timeSubmitted", "category", "actionType", "hidden", "shadowHidden", "startTime", "endTime", "views") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, 0)';
|
||||||
|
await db.prepare("run", query, ["getLabelSponsor" , 2, 0, "label01", "labeluser", 0, "sponsor", "full", 0, 0]);
|
||||||
|
await db.prepare("run", query, ["getLabelEA" , 2, 0, "label02", "labeluser", 0, "exclusive_access", "full", 0, 0]);
|
||||||
|
await db.prepare("run", query, ["getLabelSelfpromo" , 2, 0, "label03", "labeluser", 0, "selfpromo", "full", 0, 0]);
|
||||||
|
// priority override
|
||||||
|
await db.prepare("run", query, ["getLabelPriority" , 2, 0, "label04", "labeluser", 0, "sponsor", "full", 0, 0]);
|
||||||
|
await db.prepare("run", query, ["getLabelPriority" , 2, 0, "label05", "labeluser", 0, "exclusive_access", "full", 0, 0]);
|
||||||
|
await db.prepare("run", query, ["getLabelPriority" , 2, 0, "label06", "labeluser", 0, "selfpromo", "full", 0, 0]);
|
||||||
|
// locked only
|
||||||
|
await db.prepare("run", query, ["getLabelLocked" , 2, 0, "label07", "labeluser", 0, "sponsor", "full", 0, 0]);
|
||||||
|
await db.prepare("run", query, ["getLabelLocked" , 2, 0, "label08", "labeluser", 0, "exclusive_access", "full", 0, 0]);
|
||||||
|
await db.prepare("run", query, ["getLabelLocked" , 2, 1, "label09", "labeluser", 0, "selfpromo", "full", 0, 0]);
|
||||||
|
// hidden segments
|
||||||
|
await db.prepare("run", query, ["getLabelDownvote" ,-2, 0, "label10", "labeluser", 0, "selfpromo", "full", 0, 0]);
|
||||||
|
await db.prepare("run", query, ["getLabelHidden" ,2, 0, "label11", "labeluser", 0, "selfpromo", "full", 1, 0]);
|
||||||
|
await db.prepare("run", query, ["getLabelShadowHidden",2, 0, "label12", "labeluser", 0, "selfpromo", "full", 0, 1]);
|
||||||
|
// priority override2
|
||||||
|
await db.prepare("run", query, ["getLabelPriority2" , -2, 0, "label13", "labeluser", 0, "sponsor", "full", 0, 0]);
|
||||||
|
await db.prepare("run", query, ["getLabelPriority2" , 2, 0, "label14", "labeluser", 0, "exclusive_access", "full", 0, 0]);
|
||||||
|
await db.prepare("run", query, ["getLabelPriority2" , 2, 0, "label15", "labeluser", 0, "selfpromo", "full", 0, 0]);
|
||||||
|
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
function validateLabel(result: any) {
|
||||||
|
assert.strictEqual(result.length, 1);
|
||||||
|
assert.strictEqual(result[0].segment[0], 0);
|
||||||
|
assert.strictEqual(result[0].segment[1], 0);
|
||||||
|
assert.strictEqual(result[0].actionType, "full");
|
||||||
|
assert.strictEqual(result[0].userID, "labeluser");
|
||||||
|
}
|
||||||
|
|
||||||
|
const get = (videoID: string) => client.get(endpoint, { params: { videoID } });
|
||||||
|
|
||||||
|
it("Should be able to get sponsor only label", (done) => {
|
||||||
|
get("getLabelSponsor")
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
const data = res.data;
|
||||||
|
validateLabel(data);
|
||||||
|
assert.strictEqual(data[0].category, "sponsor");
|
||||||
|
assert.strictEqual(data[0].UUID, "label01");
|
||||||
|
assert.strictEqual(data[0].locked, 0);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should be able to get exclusive access only label", (done) => {
|
||||||
|
get("getLabelEA")
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
const data = res.data;
|
||||||
|
validateLabel(data);
|
||||||
|
assert.strictEqual(data[0].category, "exclusive_access");
|
||||||
|
assert.strictEqual(data[0].UUID, "label02");
|
||||||
|
assert.strictEqual(data[0].locked, 0);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should be able to get selfpromo only label", (done) => {
|
||||||
|
get("getLabelSelfpromo")
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
const data = res.data;
|
||||||
|
validateLabel(data);
|
||||||
|
assert.strictEqual(data[0].category, "selfpromo");
|
||||||
|
assert.strictEqual(data[0].UUID, "label03");
|
||||||
|
assert.strictEqual(data[0].locked, 0);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should get only sponsor if multiple segments exist", (done) => {
|
||||||
|
get("getLabelPriority")
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
const data = res.data;
|
||||||
|
validateLabel(data);
|
||||||
|
assert.strictEqual(data[0].category, "sponsor");
|
||||||
|
assert.strictEqual(data[0].UUID, "label04");
|
||||||
|
assert.strictEqual(data[0].locked, 0);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should override priority if locked", (done) => {
|
||||||
|
get("getLabelLocked")
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
const data = res.data;
|
||||||
|
validateLabel(data);
|
||||||
|
assert.strictEqual(data[0].category, "selfpromo");
|
||||||
|
assert.strictEqual(data[0].UUID, "label09");
|
||||||
|
assert.strictEqual(data[0].locked, 1);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should get highest priority category", (done) => {
|
||||||
|
get("getLabelPriority2")
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
const data = res.data;
|
||||||
|
validateLabel(data);
|
||||||
|
assert.strictEqual(data[0].category, "exclusive_access");
|
||||||
|
assert.strictEqual(data[0].UUID, "label14");
|
||||||
|
assert.strictEqual(data[0].locked, 0);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return 404 if all submissions are downvoted", (done) => {
|
||||||
|
get("getLabelDownvote")
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 404);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return 404 if all submissions are hidden", (done) => {
|
||||||
|
get("getLabelHidden")
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 404);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return 404 if all submissions are shadowhidden", (done) => {
|
||||||
|
get("getLabelShadowHidden")
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 404);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return 404 if no segment found", (done) => {
|
||||||
|
get("notarealvideo")
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 404);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should get 400 if no videoID passed in", (done) => {
|
||||||
|
client.get(endpoint)
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user