diff --git a/src/middleware/redisKeys.ts b/src/middleware/redisKeys.ts new file mode 100644 index 0000000..1335e15 --- /dev/null +++ b/src/middleware/redisKeys.ts @@ -0,0 +1,5 @@ +import { Category, VideoID } from "../types/segments.model"; + +export function skipSegmentsKey(videoID: VideoID): string { + return "segments-" + videoID; +} diff --git a/src/routes/getSkipSegments.ts b/src/routes/getSkipSegments.ts index 886c13c..41ed9ef 100644 --- a/src/routes/getSkipSegments.ts +++ b/src/routes/getSkipSegments.ts @@ -1,11 +1,14 @@ import { Request, Response } from 'express'; +import { RedisClient } from 'redis'; import { config } from '../config'; import { db, privateDB } from '../databases/databases'; +import { skipSegmentsKey } from '../middleware/redisKeys'; import { SBRecord } from '../types/lib.model'; import { Category, DBSegment, HashedIP, IPAddress, OverlappingSegmentGroup, Segment, SegmentCache, VideoData, VideoID, VideoIDHash, Visibility, VotableObject } from "../types/segments.model"; import { getHash } from '../utils/getHash'; import { getIP } from '../utils/getIP'; import { Logger } from '../utils/logger'; +import redis from '../utils/redis'; function prepareCategorySegments(req: Request, videoID: VideoID, category: Category, segments: DBSegment[], cache: SegmentCache = {shadowHiddenSegmentIPs: {}}): Segment[] { @@ -216,16 +219,29 @@ function chooseSegments(segments: DBSegment[]): DBSegment[] { * * @returns */ -function handleGetSegments(req: Request, res: Response) { - const videoID = req.query.videoID as string; +async function handleGetSegments(req: Request, res: Response): Promise { + const videoID = req.query.videoID as VideoID; // Default to sponsor // If using params instead of JSON, only one category can be pulled + console.log(req.query.categories) const categories = req.query.categories ? JSON.parse(req.query.categories as string) : req.query.category ? [req.query.category] : ['sponsor']; + // Only 404s are cached at the moment + const redisResult = await redis.getAsync(skipSegmentsKey(videoID)); + + if (redisResult.reply) { + const redisSegments = JSON.parse(redisResult.reply); + if (redisSegments?.length === 0) { + res.sendStatus(404); + Logger.debug("Using segments from cache for " + videoID); + return false; + } + } + const segments = getSegmentsByVideoID(req, videoID, categories); if (segments === null || segments === undefined) { @@ -235,18 +251,27 @@ function handleGetSegments(req: Request, res: Response) { if (segments.length === 0) { res.sendStatus(404); + + // Save in cache + redis.setAsync(skipSegmentsKey(videoID), JSON.stringify(segments)); + return false; } return segments; } -function endpoint(req: Request, res: Response): void { - let segments = handleGetSegments(req, res); +async function endpoint(req: Request, res: Response): Promise { + try { + const segments = await handleGetSegments(req, res); - if (segments) { - //send result - res.send(segments); + // If false, res.send has already been called + if (segments) { + //send result + res.send(segments); + } + } catch (err) { + res.status(500).send(); } } diff --git a/src/routes/oldGetVideoSponsorTimes.ts b/src/routes/oldGetVideoSponsorTimes.ts index a91173d..1a98314 100644 --- a/src/routes/oldGetVideoSponsorTimes.ts +++ b/src/routes/oldGetVideoSponsorTimes.ts @@ -1,8 +1,8 @@ import {handleGetSegments} from './getSkipSegments'; import {Request, Response} from 'express'; -export function oldGetVideoSponsorTimes(req: Request, res: Response) { - let segments = handleGetSegments(req, res); +export async function oldGetVideoSponsorTimes(req: Request, res: Response): Promise { + let segments = await handleGetSegments(req, res); if (segments) { // Convert to old outputs diff --git a/src/routes/postSkipSegments.ts b/src/routes/postSkipSegments.ts index 64b2124..1029f3f 100644 --- a/src/routes/postSkipSegments.ts +++ b/src/routes/postSkipSegments.ts @@ -11,6 +11,8 @@ import {getFormattedTime} from '../utils/getFormattedTime'; import {isUserTrustworthy} from '../utils/isUserTrustworthy'; import {dispatchEvent} from '../utils/webhookUtils'; import {Request, Response} from 'express'; +import { skipSegmentsKey } from '../middleware/redisKeys'; +import redis from '../utils/redis'; function sendWebhookNotification(userID: string, videoID: string, UUID: string, submissionCount: number, youtubeData: any, {submissionStart, submissionEnd}: { submissionStart: number; submissionEnd: number; }, segmentInfo: any) { @@ -496,6 +498,9 @@ export async function postSkipSegments(req: Request, res: Response) { //add to private db as well privateDB.prepare('run', "INSERT INTO sponsorTimes VALUES(?, ?, ?)", [videoID, hashedIP, timeSubmitted]); + + // Clear redis cache for this video + redis.delAsync(skipSegmentsKey(videoID)); } catch (err) { //a DB change probably occurred res.sendStatus(502); diff --git a/src/routes/voteOnSponsorTime.ts b/src/routes/voteOnSponsorTime.ts index 1cc9cda..17710c7 100644 --- a/src/routes/voteOnSponsorTime.ts +++ b/src/routes/voteOnSponsorTime.ts @@ -11,6 +11,9 @@ import {getIP} from '../utils/getIP'; import {getHash} from '../utils/getHash'; import {config} from '../config'; import { UserID } from '../types/user.model'; +import redis from '../utils/redis'; +import { skipSegmentsKey } from '../middleware/redisKeys'; +import { VideoID } from '../types/segments.model'; const voteTypes = { normal: 0, @@ -335,7 +338,8 @@ export async function voteOnSponsorTime(req: Request, res: Response) { } //check if the increment amount should be multiplied (downvotes have more power if there have been many views) - const row = db.prepare('get', "SELECT votes, views FROM sponsorTimes WHERE UUID = ?", [UUID]); + const row = db.prepare('get', "SELECT videoID, votes, views FROM sponsorTimes WHERE UUID = ?", [UUID]) as + {videoID: VideoID, votes: number, views: number}; if (voteTypeEnum === voteTypes.normal) { if ((isVIP || isOwnSubmission) && incrementAmount < 0) { @@ -383,6 +387,9 @@ export async function voteOnSponsorTime(req: Request, res: Response) { db.prepare('run', "UPDATE sponsorTimes SET locked = 0 WHERE UUID = ?", [UUID]); } + // Clear redis cache for this video + redis.delAsync(skipSegmentsKey(row.videoID)); + //for each positive vote, see if a hidden submission can be shown again if (incrementAmount > 0 && voteTypeEnum === voteTypes.normal) { //find the UUID that submitted the submission that was voted on diff --git a/src/utils/redis.ts b/src/utils/redis.ts index 3144ae6..358ebb1 100644 --- a/src/utils/redis.ts +++ b/src/utils/redis.ts @@ -2,15 +2,33 @@ import {config} from '../config'; import {Logger} from './logger'; import redis, {Callback} from 'redis'; -let exportObject = { - get: (key: string, callback?: Callback) => callback(null, undefined), - set: (key: string, value: string, callback?: Callback) => callback(null, undefined) +interface RedisSB { + get(key: string, callback?: Callback): void; + getAsync?(key: string): Promise<{err: Error | null, reply: string | null}>; + set(key: string, value: string, callback?: Callback): void; + setAsync?(key: string, value: string): Promise<{err: Error | null, reply: string | null}>; + delAsync?(...keys: [string]): Promise; +} + +let exportObject: RedisSB = { + get: (key, callback?) => callback(null, undefined), + getAsync: (key) => + new Promise((resolve) => resolve({err: null, reply: undefined})), + set: (key, value, callback) => callback(null, undefined), + setAsync: (key, value) => + new Promise((resolve) => resolve({err: null, reply: undefined})), + delAsync: (...keys) => + new Promise((resolve) => resolve(null)), }; if (config.redis) { Logger.info('Connected to redis'); const client = redis.createClient(config.redis); exportObject = client; + + exportObject.getAsync = (key) => new Promise((resolve) => client.get(key, (err, reply) => resolve({err, reply}))); + exportObject.setAsync = (key, value) => new Promise((resolve) => client.set(key, value, (err, reply) => resolve({err, reply}))); + exportObject.delAsync = (...keys) => new Promise((resolve) => client.del(keys, (err) => resolve(err))); } export default exportObject;