From 3d30eea1cb0cb27f2b93d85cf6f7a96f48623069 Mon Sep 17 00:00:00 2001 From: Michael C Date: Tue, 7 Sep 2021 00:13:52 -0400 Subject: [PATCH] add categoryStats and typeStats --- src/routes/getUserStats.ts | 82 ++++++++++++++++++++++++++++---------- test/cases/getUserStats.ts | 60 ++++++++++++++++++---------- 2 files changed, 99 insertions(+), 43 deletions(-) diff --git a/src/routes/getUserStats.ts b/src/routes/getUserStats.ts index 5c9b563..f2d304f 100644 --- a/src/routes/getUserStats.ts +++ b/src/routes/getUserStats.ts @@ -1,29 +1,68 @@ import {db} from "../databases/databases"; import {getHash} from "../utils/getHash"; import {Request, Response} from "express"; -import { HashedUserID, UserID } from "../types/user.model"; -import { Category } from "../types/segments.model"; +import {HashedUserID, UserID} from "../types/user.model"; import {config} from "../config"; -const maxRewardTime = config.maxRewardTimePerSegmentInSeconds; +import { Logger } from "../utils/logger"; +type nestedObj = Record>; +const maxRewardTimePerSegmentInSeconds = config.maxRewardTimePerSegmentInSeconds ?? 86400; -async function dbGetCategorySummary(userID: HashedUserID, category: Category): Promise<{ minutesSaved: number, segmentCount: number }> { +async function dbGetUserSummary(userID: HashedUserID, categoryStats: boolean, typeStats: boolean) { + let additionalQuery = ""; + if (categoryStats) { + additionalQuery += ` + SUM(CASE WHEN category = 'sponsor' THEN 1 ELSE 0 END) as "categorySumSponsor", + SUM(CASE WHEN category = 'intro' THEN 1 ELSE 0 END) as "categorySumIntro", + SUM(CASE WHEN category = 'outro' THEN 1 ELSE 0 END) as "categorySumOutro", + SUM(CASE WHEN category = 'interaction' THEN 1 ELSE 0 END) as "categorySumInteraction", + SUM(CASE WHEN category = 'selfpromo' THEN 1 ELSE 0 END) as "categorySelfpromo", + SUM(CASE WHEN category = 'music_offtopic' THEN 1 ELSE 0 END) as "categoryMusicOfftopic", + SUM(CASE WHEN category = 'preview' THEN 1 ELSE 0 END) as "categorySumPreview", + SUM(CASE WHEN category = 'poi_highlight' THEN 1 ELSE 0 END) as "categorySumHighlight",`; + } + if (typeStats) { + additionalQuery += ` + SUM(CASE WHEN actionType = 'skip' THEN 1 ELSE 0 END) as 'typeSumSkip', + SUM(CASE WHEN actionType = 'mute' THEN 1 ELSE 0 END) as 'typeSumMute',`; + } try { - const row = await db.prepare("get", - `SELECT SUM(((CASE WHEN "endTime" - "startTime" > ? THEN ? ELSE "endTime" - "startTime" END) / 60) * "views") as "minutesSaved", - count(*) as "segmentCount" FROM "sponsorTimes" - WHERE "userID" = ? AND "category" = ? AND "votes" > -2 AND "shadowHidden" != 1`, [maxRewardTime, maxRewardTime, userID, category]); - if (row.minutesSaved != null) { - return { - minutesSaved: row.minutesSaved, - segmentCount: row.segmentCount, - }; - } else { - return { - minutesSaved: 0, - segmentCount: 0, + const row = await db.prepare("get", ` + SELECT SUM(((CASE WHEN "endTime" - "startTime" > ? THEN ? ELSE "endTime" - "startTime" END) / 60) * "views") as "minutesSaved", + ${additionalQuery} + count(*) as "segmentCount" + FROM "sponsorTimes" + WHERE "userID" = ? AND votes > -2 AND shadowHidden !=1`, + [maxRewardTimePerSegmentInSeconds, maxRewardTimePerSegmentInSeconds, userID]); + const source = (row.minutesSaved != null) ? row : {}; + const handler = { get: (target: Record, name: string) => target?.[name] || 0 }; + const proxy = new Proxy(source, handler); + const result = {} as nestedObj; + + result.overallStats = { + minutesSaved: proxy.minutesSaved, + segmentCount: proxy.segmentCount, + }; + if (categoryStats) { + result.categoryCount = { + sponsor: proxy.categorySumSponsor, + intro: proxy.categorySumIntro, + outro: proxy.categorySumOutro, + interaction: proxy.categorySumInteraction, + selfpromo: proxy.categorySelfpromo, + music_offtopic: proxy.categoryMusicOfftopic, + preview: proxy.categorySumPreview, + poi_highlight: proxy.categorySumHighlight, }; } + if (typeStats) { + result.actionTypeCount = { + skip: proxy.typeSumSkip, + mute: proxy.typeSumMute, + }; + } + return result; } catch (err) { + Logger.error(err as string); return null; } } @@ -40,17 +79,18 @@ async function dbGetUsername(userID: HashedUserID) { export async function getUserStats(req: Request, res: Response): Promise { const userID = req.query.userID as UserID; const hashedUserID: HashedUserID = userID ? getHash(userID) : req.query.publicUserID as HashedUserID; + const categoryStats = req.query.categoryStats == "true"; + const typeStats = req.query.typeStats == "true"; if (hashedUserID == undefined) { //invalid request return res.status(400).send("Invalid userID or publicUserID parameter"); } + const segmentSummary = await dbGetUserSummary(hashedUserID, categoryStats, typeStats); const responseObj = { userID: hashedUserID, userName: await dbGetUsername(hashedUserID), - } as Record | string >; - for (const category of config.categoryList) { - responseObj[category] = await dbGetCategorySummary(hashedUserID, category as Category); - } + ...segmentSummary, + } as Record; return res.send(responseObj); } diff --git a/test/cases/getUserStats.ts b/test/cases/getUserStats.ts index 417e21b..d689198 100644 --- a/test/cases/getUserStats.ts +++ b/test/cases/getUserStats.ts @@ -3,6 +3,7 @@ import {Done, getbaseURL} from "../utils"; import {db} from "../../src/databases/databases"; import {getHash} from "../../src/utils/getHash"; import assert from "assert"; +const includeAllStats = "&categoryStats=true&typeStats=true"; describe("getUserStats", () => { before(async () => { @@ -31,30 +32,31 @@ describe("getUserStats", () => { .catch(err => done(err)); }); - it("Should be able to get user info", (done: Done) => { - fetch(`${getbaseURL()}/api/userStats?userID=getuserstats_user_01`) + it("Should be able to get all user info", (done: Done) => { + fetch(`${getbaseURL()}/api/userStats?userID=getuserstats_user_01${includeAllStats}`) .then(async res => { assert.strictEqual(res.status, 200); const expected = { userName: "Username user 01", userID: getHash("getuserstats_user_01"), - sponsor: { - minutesSaved: 1, segmentCount: 1, - }, selfpromo: { - minutesSaved: 2, segmentCount: 1, - }, interaction: { - minutesSaved: 3, segmentCount: 1, - }, intro: { - minutesSaved: 4, segmentCount: 1, - }, outro: { - minutesSaved: 5, segmentCount: 1, - }, preview: { - minutesSaved: 6, segmentCount: 1, - }, music_offtopic: { - minutesSaved: 7, segmentCount: 1, - }, poi_highlight: { - minutesSaved: 0, segmentCount: 1, + categoryCount: { + sponsor: 1, + selfpromo: 1, + interaction: 1, + intro: 1, + outro: 1, + preview: 1, + music_offtopic: 1, + poi_highlight: 1, }, + actionTypeCount: { + mute: 0, + skip: 8 + }, + overallStats: { + minutesSaved: 28, + segmentCount: 8 + } }; const data = await res.json(); assert.deepStrictEqual(data, expected); @@ -68,8 +70,8 @@ describe("getUserStats", () => { .then(async res => { assert.strictEqual(res.status, 200); const data = await res.json(); - for (const value in data) { - if (data[value]?.minutesSaved || data[value]?.segmentCount) { + for (const value in data.overallStats) { + if (data[value]) { done(`returned non-zero for ${value}`); } } @@ -83,8 +85,8 @@ describe("getUserStats", () => { .then(async res => { assert.strictEqual(res.status, 200); const data = await res.json(); - for (const value in data) { - if (data[value]?.minutesSaved || data[value]?.segmentCount) { + for (const value in data.overallStats) { + if (data[value]) { done(`returned non-zero for ${value}`); } } @@ -92,4 +94,18 @@ describe("getUserStats", () => { }) .catch(err => done(err)); }); + + it("Should not get extra stats if not requested", (done: Done) => { + fetch(`${getbaseURL()}/api/userStats?userID=getuserstats_user_01`) + .then(async res => { + assert.strictEqual(res.status, 200); + const data = await res.json(); + // check for categoryCount + if (data.categoryCount || data.actionTypeCount) { + done("returned extra stats"); + } + done(); + }) + .catch(err => done(err)); + }); });