Skip to content

Commit 4bd2204

Browse files
committed
optimize skipSegments, add eTag
- moved skipSegments parameter parsing to new file - added oldGetVideoSponsorTimes to getSkipSegments.ts
1 parent 112b1c8 commit 4bd2204

File tree

7 files changed

+130
-163
lines changed

7 files changed

+130
-163
lines changed

src/app.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import express, { Request, RequestHandler, Response, Router } from "express";
22
import { config } from "./config";
33
import { oldSubmitSponsorTimes } from "./routes/oldSubmitSponsorTimes";
4-
import { oldGetVideoSponsorTimes } from "./routes/oldGetVideoSponsorTimes";
54
import { postSegmentShift } from "./routes/postSegmentShift";
65
import { postWarning } from "./routes/postWarning";
76
import { getIsUserVIP } from "./routes/getIsUserVIP";
@@ -21,13 +20,13 @@ import { viewedVideoSponsorTime } from "./routes/viewedVideoSponsorTime";
2120
import { voteOnSponsorTime, getUserID as voteGetUserID } from "./routes/voteOnSponsorTime";
2221
import { getSkipSegmentsByHash } from "./routes/getSkipSegmentsByHash";
2322
import { postSkipSegments } from "./routes/postSkipSegments";
24-
import { endpoint as getSkipSegments } from "./routes/getSkipSegments";
23+
import { getSkipSegments, oldGetVideoSponsorTimes } from "./routes/getSkipSegments";
2524
import { userCounter } from "./middleware/userCounter";
2625
import { loggerMiddleware } from "./middleware/logger";
2726
import { corsMiddleware } from "./middleware/cors";
2827
import { apiCspMiddleware } from "./middleware/apiCsp";
2928
import { rateLimitMiddleware } from "./middleware/requestRateLimit";
30-
import dumpDatabase, { appExportPath, downloadFile } from "./routes/dumpDatabase";
29+
import dumpDatabase from "./routes/dumpDatabase";
3130
import { endpoint as getSegmentInfo } from "./routes/getSegmentInfo";
3231
import { postClearCache } from "./routes/postClearCache";
3332
import { addUnlistedVideo } from "./routes/addUnlistedVideo";

src/middleware/etag.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { skipSegmentsHashKey, skipSegmentsKey, videoLabelsHashKey, videoLabelsKe
55

66
type hashType = "skipSegments" | "skipSegmentsHash" | "videoLabel" | "videoLabelHash";
77
type ETag = `${hashType};${VideoIDHash};${Service};${number}`;
8+
type hashKey = string | VideoID | VideoIDHash;
89

910
export function cacheMiddlware(req: Request, res: Response, next: NextFunction): void {
1011
const reqEtag = req.get("If-None-Match") as string;
@@ -25,7 +26,7 @@ export function cacheMiddlware(req: Request, res: Response, next: NextFunction):
2526
.catch(next);
2627
}
2728

28-
function getLastModified(hashType: hashType, hashKey: (VideoID | VideoIDHash), service: Service): Promise<Date | null> {
29+
function getLastModified(hashType: hashType, hashKey: hashKey, service: Service): Promise<Date | null> {
2930
let redisKey: string | null;
3031
if (hashType === "skipSegments") redisKey = skipSegmentsKey(hashKey as VideoID, service);
3132
else if (hashType === "skipSegmentsHash") redisKey = skipSegmentsHashKey(hashKey as VideoIDHash, service);
@@ -35,7 +36,7 @@ function getLastModified(hashType: hashType, hashKey: (VideoID | VideoIDHash), s
3536
return QueryCacher.getKeyLastModified(redisKey);
3637
}
3738

38-
export async function getEtag(hashType: hashType, hashKey: VideoIDHash, service: Service): Promise<ETag> {
39+
export async function getEtag(hashType: hashType, hashKey: hashKey, service: Service): Promise<ETag> {
3940
const lastModified = await getLastModified(hashType, hashKey, service);
4041
return `${hashType};${hashKey};${service};${lastModified.getTime()}` as ETag;
4142
}

src/routes/getSkipSegments.ts

Lines changed: 42 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import { QueryCacher } from "../utils/queryCacher";
1212
import { getReputation } from "../utils/reputation";
1313
import { getService } from "../utils/getService";
1414
import { promiseOrTimeout } from "../utils/promise";
15-
15+
import { parseSkipSegments } from "../utils/parseSkipSegments";
16+
import { getEtag } from "../middleware/etag";
1617

1718
async function prepareCategorySegments(req: Request, videoID: VideoID, service: Service, segments: DBSegment[], cache: SegmentCache = { shadowHiddenSegmentIPs: {} }, useCache: boolean): Promise<Segment[]> {
1819
const shouldFilter: boolean[] = await Promise.all(segments.map(async (segment) => {
@@ -86,9 +87,6 @@ async function getSegmentsByVideoID(req: Request, videoID: VideoID, categories:
8687
}
8788

8889
try {
89-
categories = categories.filter((category) => !/[^a-z|_|-]/.test(category));
90-
if (categories.length === 0) return null;
91-
9290
const segments: DBSegment[] = (await getSegmentsFromDBByVideoID(videoID, service))
9391
.map((segment: DBSegment) => {
9492
if (filterRequiredSegments(segment.UUID, requiredSegments)) segment.required = true;
@@ -139,9 +137,6 @@ async function getSegmentsByHash(req: Request, hashedVideoIDPrefix: VideoIDHash,
139137
try {
140138
type SegmentPerVideoID = SBRecord<VideoID, { segments: DBSegment[] }>;
141139

142-
categories = categories.filter((category) => !(/[^a-z|_|-]/.test(category)));
143-
if (categories.length === 0) return null;
144-
145140
const segmentPerVideoID: SegmentPerVideoID = (await getSegmentsFromDBByHash(hashedVideoIDPrefix, service))
146141
.reduce((acc: SegmentPerVideoID, segment: DBSegment) => {
147142
acc[segment.videoID] = acc[segment.videoID] || {
@@ -396,75 +391,59 @@ function splitPercentOverlap(groups: OverlappingSegmentGroup[]): OverlappingSegm
396391
});
397392
}
398393

399-
/**
400-
*
401-
* Returns what would be sent to the client.
402-
* Will respond with errors if required. Returns false if it errors.
403-
*
404-
* @param req
405-
* @param res
406-
*
407-
* @returns
408-
*/
409-
async function handleGetSegments(req: Request, res: Response): Promise<Segment[] | false> {
394+
async function getSkipSegments(req: Request, res: Response): Promise<Response> {
410395
const videoID = req.query.videoID as VideoID;
411396
if (!videoID) {
412-
res.status(400).send("videoID not specified");
413-
return false;
414-
}
415-
// Default to sponsor
416-
// If using params instead of JSON, only one category can be pulled
417-
const categories: Category[] = req.query.categories
418-
? JSON.parse(req.query.categories as string)
419-
: req.query.category
420-
? Array.isArray(req.query.category)
421-
? req.query.category
422-
: [req.query.category]
423-
: ["sponsor"];
424-
if (!Array.isArray(categories)) {
425-
res.status(400).send("Categories parameter does not match format requirements.");
426-
return false;
397+
return res.status(400).send("videoID not specified");
427398
}
428399

429-
const actionTypes: ActionType[] = req.query.actionTypes
430-
? JSON.parse(req.query.actionTypes as string)
431-
: req.query.actionType
432-
? Array.isArray(req.query.actionType)
433-
? req.query.actionType
434-
: [req.query.actionType]
435-
: [ActionType.Skip];
436-
if (!Array.isArray(actionTypes)) {
437-
res.status(400).send("actionTypes parameter does not match format requirements.");
438-
return false;
400+
const parseResult = parseSkipSegments(req);
401+
if (parseResult.errors.length > 0) {
402+
return res.status(400).send(parseResult.errors);
439403
}
440404

441-
const requiredSegments: SegmentUUID[] = req.query.requiredSegments
442-
? JSON.parse(req.query.requiredSegments as string)
443-
: req.query.requiredSegment
444-
? Array.isArray(req.query.requiredSegment)
445-
? req.query.requiredSegment
446-
: [req.query.requiredSegment]
447-
: [];
448-
if (!Array.isArray(requiredSegments)) {
449-
res.status(400).send("requiredSegments parameter does not match format requirements.");
450-
return false;
405+
const { categories, actionTypes, requiredSegments, service } = parseResult;
406+
const segments = await getSegmentsByVideoID(req, videoID, categories, actionTypes, requiredSegments, service);
407+
408+
if (segments === null || segments === undefined) {
409+
return res.sendStatus(500);
410+
} else if (segments.length === 0) {
411+
return res.sendStatus(404);
451412
}
452413

453-
const service = getService(req.query.service, req.body.service);
414+
await getEtag("skipSegments", (videoID as string), service)
415+
.then(etag => res.set("ETag", etag))
416+
.catch(() => null);
417+
return res.send(segments);
418+
}
454419

455-
const segments = await getSegmentsByVideoID(req, videoID, categories, actionTypes, requiredSegments, service);
420+
async function oldGetVideoSponsorTimes(req: Request, res: Response): Promise<Response> {
421+
const videoID = req.query.videoID as VideoID;
422+
if (!videoID) {
423+
return res.status(400).send("videoID not specified");
424+
}
425+
426+
const segments = await getSegmentsByVideoID(req, videoID, ["sponsor"] as Category[], [ActionType.Skip], [], Service.YouTube);
456427

457428
if (segments === null || segments === undefined) {
458-
res.sendStatus(500);
459-
return false;
429+
return res.sendStatus(500);
430+
} else if (segments.length === 0) {
431+
return res.sendStatus(404);
460432
}
461433

462-
if (segments.length === 0) {
463-
res.sendStatus(404);
464-
return false;
434+
// Convert to old outputs
435+
const sponsorTimes = [];
436+
const UUIDs = [];
437+
438+
for (const segment of segments) {
439+
sponsorTimes.push(segment.segment);
440+
UUIDs.push(segment.UUID);
465441
}
466442

467-
return segments;
443+
return res.send({
444+
sponsorTimes,
445+
UUIDs,
446+
});
468447
}
469448

470449
const filterRequiredSegments = (UUID: SegmentUUID, requiredSegments: SegmentUUID[]): boolean => {
@@ -474,25 +453,9 @@ const filterRequiredSegments = (UUID: SegmentUUID, requiredSegments: SegmentUUID
474453
return false;
475454
};
476455

477-
async function endpoint(req: Request, res: Response): Promise<Response> {
478-
try {
479-
const segments = await handleGetSegments(req, res);
480-
481-
// If false, res.send has already been called
482-
if (segments) {
483-
//send result
484-
return res.send(segments);
485-
}
486-
} catch (err) /* istanbul ignore next */ {
487-
if (err instanceof SyntaxError) {
488-
return res.status(400).send("Categories parameter does not match format requirements.");
489-
} else return res.sendStatus(500);
490-
}
491-
}
492-
493456
export {
494457
getSegmentsByVideoID,
495458
getSegmentsByHash,
496-
endpoint,
497-
handleGetSegments
459+
getSkipSegments,
460+
oldGetVideoSponsorTimes
498461
};

src/routes/getSkipSegmentsByHash.ts

Lines changed: 6 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { hashPrefixTester } from "../utils/hashPrefixTester";
22
import { getSegmentsByHash } from "./getSkipSegments";
33
import { Request, Response } from "express";
4-
import { ActionType, Category, SegmentUUID, VideoIDHash, Service } from "../types/segments.model";
5-
import { getService } from "../utils/getService";
4+
import { VideoIDHash } from "../types/segments.model";
65
import { Logger } from "../utils/logger";
6+
import { parseSkipSegments } from "../utils/parseSkipSegments";
77
import { getEtag } from "../middleware/etag";
88

99
export async function getSkipSegmentsByHash(req: Request, res: Response): Promise<Response> {
@@ -13,58 +13,11 @@ export async function getSkipSegmentsByHash(req: Request, res: Response): Promis
1313
}
1414
hashPrefix = hashPrefix.toLowerCase() as VideoIDHash;
1515

16-
let categories: Category[] = [];
17-
try {
18-
categories = req.query.categories
19-
? JSON.parse(req.query.categories as string)
20-
: req.query.category
21-
? Array.isArray(req.query.category)
22-
? req.query.category
23-
: [req.query.category]
24-
: ["sponsor"];
25-
if (!Array.isArray(categories)) {
26-
return res.status(400).send("Categories parameter does not match format requirements.");
27-
}
28-
} catch(error) {
29-
return res.status(400).send("Bad parameter: categories (invalid JSON)");
16+
const parseResult = parseSkipSegments(req);
17+
if (parseResult.errors.length > 0) {
18+
return res.status(400).send(parseResult.errors);
3019
}
31-
32-
let actionTypes: ActionType[] = [];
33-
try {
34-
actionTypes = req.query.actionTypes
35-
? JSON.parse(req.query.actionTypes as string)
36-
: req.query.actionType
37-
? Array.isArray(req.query.actionType)
38-
? req.query.actionType
39-
: [req.query.actionType]
40-
: [ActionType.Skip];
41-
if (!Array.isArray(actionTypes)) {
42-
return res.status(400).send("actionTypes parameter does not match format requirements.");
43-
}
44-
} catch(error) {
45-
return res.status(400).send("Bad parameter: actionTypes (invalid JSON)");
46-
}
47-
48-
let requiredSegments: SegmentUUID[] = [];
49-
try {
50-
requiredSegments = req.query.requiredSegments
51-
? JSON.parse(req.query.requiredSegments as string)
52-
: req.query.requiredSegment
53-
? Array.isArray(req.query.requiredSegment)
54-
? req.query.requiredSegment
55-
: [req.query.requiredSegment]
56-
: [];
57-
if (!Array.isArray(requiredSegments)) {
58-
return res.status(400).send("requiredSegments parameter does not match format requirements.");
59-
}
60-
} catch(error) {
61-
return res.status(400).send("Bad parameter: requiredSegments (invalid JSON)");
62-
}
63-
64-
const service: Service = getService(req.query.service, req.body.service);
65-
66-
// filter out none string elements, only flat array with strings is valid
67-
categories = categories.filter((item: any) => typeof item === "string");
20+
const { categories, actionTypes, requiredSegments, service } = parseResult;
6821

6922
// Get all video id's that match hash prefix
7023
const segments = await getSegmentsByHash(req, hashPrefix, categories, actionTypes, requiredSegments, service);

src/routes/oldGetVideoSponsorTimes.ts

Lines changed: 0 additions & 24 deletions
This file was deleted.

src/routes/viewedVideoSponsorTime.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import { db } from "../databases/databases";
22
import { Request, Response } from "express";
33

44
export async function viewedVideoSponsorTime(req: Request, res: Response): Promise<Response> {
5-
const UUID = req.query.UUID;
5+
const UUID = req.query?.UUID;
66

7-
if (UUID == undefined) {
7+
if (!UUID) {
88
//invalid request
99
return res.sendStatus(400);
1010
}

0 commit comments

Comments
 (0)