diff --git a/src/routes/ratings/getRating.ts b/src/routes/ratings/getRating.ts index ec7e9f0..edaae8e 100644 --- a/src/routes/ratings/getRating.ts +++ b/src/routes/ratings/getRating.ts @@ -17,11 +17,24 @@ interface DBRating { } export async function getRating(req: Request, res: Response): Promise { - let hashPrefix = req.params.prefix as VideoIDHash; - if (!hashPrefix || !hashPrefixTester(hashPrefix)) { + let hashPrefixes: VideoIDHash[] = []; + try { + hashPrefixes = req.query.hashPrefixes + ? JSON.parse(req.query.hashPrefixes as string) + : Array.isArray(req.query.prefix) + ? req.query.prefix + : [req.query.prefix ?? req.params.prefix]; + if (!Array.isArray(hashPrefixes)) { + return res.status(400).send("hashPrefixes parameter does not match format requirements."); + } + + hashPrefixes.map((hashPrefix) => hashPrefix?.toLowerCase()); + } catch(error) { + return res.status(400).send("Bad parameter: hashPrefixes (invalid JSON)"); + } + if (hashPrefixes.some((hashPrefix) => !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 { @@ -44,7 +57,7 @@ export async function getRating(req: Request, res: Response): Promise const service: Service = getService(req.query.service, req.body.service); try { - const ratings = (await getRatings(hashPrefix, service)) + const ratings = (await getRatings(hashPrefixes, service)) .filter((rating) => types.includes(rating.type)) .map((rating) => ({ videoID: rating.videoID, @@ -53,23 +66,25 @@ export async function getRating(req: Request, res: Response): Promise 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 { - const fetchFromDB = () => db +function getRatings(hashPrefixes: VideoIDHash[], service: Service): Promise { + const fetchFromDB = (hashPrefixes: VideoIDHash[]) => db .prepare( "all", - `SELECT "videoID", "hashedVideoID", "type", "count" FROM "ratings" WHERE "hashedVideoID" LIKE ? AND "service" = ? ORDER BY "hashedVideoID"`, - [`${hashPrefix}%`, service] + `SELECT "videoID", "hashedVideoID", "type", "count" FROM "ratings" WHERE "hashedVideoID" ~* ? AND "service" = ? ORDER BY "hashedVideoID"`, + [`^(?:${hashPrefixes.join("|")})`, service] ) as Promise; - return (hashPrefix.length === 4) - ? QueryCacher.get(fetchFromDB, ratingHashKey(hashPrefix, service)) - : fetchFromDB(); + return (hashPrefixes.every((hashPrefix) => hashPrefix.length === 4)) + ? QueryCacher.getAndSplit(fetchFromDB, (prefix) => ratingHashKey(prefix, service), "hashedVideoID", hashPrefixes) + : fetchFromDB(hashPrefixes); } \ No newline at end of file diff --git a/src/utils/queryCacher.ts b/src/utils/queryCacher.ts index d8ecb96..d5ea110 100644 --- a/src/utils/queryCacher.ts +++ b/src/utils/queryCacher.ts @@ -10,6 +10,7 @@ async function get(fetchFromDB: () => Promise, key: string): Promise { if (!err && reply) { try { Logger.debug(`Got data from redis: ${reply}`); + return JSON.parse(reply); } catch (e) { // If all else, continue on to fetching from the database @@ -22,6 +23,53 @@ async function get(fetchFromDB: () => Promise, key: string): Promise { return data; } +/** + * Gets from redis for all specified values and splits the result before adding it to redis cache + */ +async function getAndSplit(fetchFromDB: (values: U[]) => Promise>, keyGenerator: (value: U) => string, splitKey: string, values: U[]): Promise> { + const cachedValues = await Promise.all(values.map(async (value) => { + const key = keyGenerator(value); + const { err, reply } = await redis.getAsync(key); + + if (!err && reply) { + try { + Logger.debug(`Got data from redis: ${reply}`); + + return { + value, + result: JSON.parse(reply) + }; + } catch (e) { + // eslint-disable-next-line no-console + } + } + + return { + value, + result: null + }; + })); + + const data = await fetchFromDB( + cachedValues.filter((cachedValue) => cachedValue.result === null) + .map((cachedValue) => cachedValue.value)); + + new Promise(() => { + const newResults: Record = {}; + for (const item of data) { + const key = (item as unknown as Record)[splitKey]; + newResults[key] ??= []; + newResults[key].push(item); + } + + for (const key in newResults) { + redis.setAsync(keyGenerator(key as unknown as U), JSON.stringify(newResults[key])); + } + }); + + return data.concat(cachedValues.map((cachedValue) => cachedValue.result).filter((result) => result !== null)); +} + function clearSegmentCache(videoInfo: { videoID: VideoID; hashedVideoID: VideoIDHash; service: Service; userID?: UserID; }): void { if (videoInfo) { redis.delAsync(skipSegmentsKey(videoInfo.videoID, videoInfo.service)); @@ -38,6 +86,7 @@ function clearRatingCache(videoInfo: { hashedVideoID: VideoIDHash; service: Serv export const QueryCacher = { get, + getAndSplit, clearSegmentCache, clearRatingCache }; \ No newline at end of file