Skip to content

Commit 31e678f

Browse files
committed
Store titles for casual vote submissions
When an uploader changes the title, it will reset the casual votes
1 parent d44ce3c commit 31e678f

File tree

10 files changed

+141
-26
lines changed

10 files changed

+141
-26
lines changed

databases/_sponsorTimes.db.sql

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,15 @@ CREATE TABLE IF NOT EXISTS "casualVotes" (
9595
"timeSubmitted" INTEGER NOT NULL
9696
);
9797

98+
CREATE TABLE IF NOT EXISTS "casualVoteTitles" (
99+
"videoID" TEXT NOT NULL,
100+
"service" TEXT NOT NULL,
101+
"id" INTEGER NOT NULL,
102+
"hashedVideoID" TEXT NOT NULL,
103+
"title" TEXT NOT NULL,
104+
PRIMARY KEY("videoID", "service", "id")
105+
);
106+
98107
CREATE EXTENSION IF NOT EXISTS pgcrypto; --!sqlite-ignore
99108
CREATE EXTENSION IF NOT EXISTS pg_trgm; --!sqlite-ignore
100109

databases/_upgrade_private_13.sql

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
BEGIN TRANSACTION;
2+
3+
ALTER TABLE "casualVotes" ADD "titleID" INTEGER default 0;
4+
5+
UPDATE "config" SET value = 13 WHERE key = 'version';
6+
7+
COMMIT;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
BEGIN TRANSACTION;
2+
3+
ALTER TABLE "casualVotes" ADD "titleID" INTEGER default 0;
4+
5+
UPDATE "config" SET value = 43 WHERE key = 'version';
6+
7+
COMMIT;

src/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@ addDefaults(config, {
153153
{
154154
name: "casualVotes",
155155
order: "timeSubmitted"
156+
},
157+
{
158+
name: "casualVoteTitles"
156159
}]
157160
},
158161
diskCacheURL: null,

src/routes/getBranding.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,10 @@ export async function getVideoBranding(res: Response, videoID: VideoID, service:
5353

5454
const getCasualVotes = () => db.prepare(
5555
"all",
56-
`SELECT "category", "upvotes" FROM "casualVotes"
57-
WHERE "videoID" = ? AND "service" = ?
58-
ORDER BY "timeSubmitted" ASC`,
56+
`SELECT "casualVotes"."category", "casualVotes"."upvotes", "casualVoteTitles"."title"
57+
FROM "casualVotes" LEFT JOIN "casualVoteTitles" ON "casualVotes"."videoID" = "casualVoteTitles"."videoID" AND "casualVotes"."service" = "casualVoteTitles"."service" AND "casualVotes"."titleID" = "casualVoteTitles"."id"
58+
WHERE "casualVotes"."videoID" = ? AND "casualVotes"."service" = ?
59+
ORDER BY "casualVotes"."timeSubmitted" ASC`,
5960
[videoID, service],
6061
{ useReplica: true }
6162
) as Promise<CasualVoteDBResult[]>;
@@ -131,9 +132,10 @@ export async function getVideoBrandingByHash(videoHashPrefix: VideoIDHash, servi
131132

132133
const getCasualVotes = () => db.prepare(
133134
"all",
134-
`SELECT "videoID", "category", "upvotes" FROM "casualVotes"
135-
WHERE "hashedVideoID" LIKE ? AND "service" = ?
136-
ORDER BY "timeSubmitted" ASC`,
135+
`SELECT "casualVotes"."videoID", "casualVotes"."category", "casualVotes"."upvotes", "casualVoteTitles"."title"
136+
FROM "casualVotes" LEFT JOIN "casualVoteTitles" ON "casualVotes"."videoID" = "casualVoteTitles"."videoID" AND "casualVotes"."service" = "casualVoteTitles"."service" AND "casualVotes"."titleID" = "casualVoteTitles"."id"
137+
WHERE "casualVotes"."hashedVideoID" LIKE ? AND "casualVotes"."service" = ?
138+
ORDER BY "casualVotes"."timeSubmitted" ASC`,
137139
[`${videoHashPrefix}%`, service],
138140
{ useReplica: true }
139141
) as Promise<CasualVoteHashDBResult[]>;
@@ -233,7 +235,8 @@ async function filterAndSortBranding(videoID: VideoID, returnUserID: boolean, fe
233235
const casualDownvotes = dbCasualVotes.filter((r) => r.category === "downvote")[0];
234236
const casualVotes = dbCasualVotes.filter((r) => r.category !== "downvote").map((r) => ({
235237
id: r.category,
236-
count: r.upvotes - (casualDownvotes?.upvotes ?? 0)
238+
count: r.upvotes - (casualDownvotes?.upvotes ?? 0),
239+
title: r.title
237240
})).filter((a) => a.count > 0);
238241

239242
const videoDuration = dbSegments.filter(s => s.videoDuration !== 0)[0]?.videoDuration ?? null;

src/routes/postCasual.ts

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ interface ExistingVote {
2222
export async function postCasual(req: Request, res: Response) {
2323
const { videoID, userID, downvote } = req.body as CasualVoteSubmission;
2424
let categories = req.body.categories as CasualCategory[];
25+
const title = (req.body.title as string)?.toLowerCase();
2526
const service = getService(req.body.service);
2627

2728
if (downvote) {
@@ -50,19 +51,41 @@ export async function postCasual(req: Request, res: Response) {
5051
return res.status(200).send("OK");
5152
}
5253

54+
let titleID = 0;
55+
if (title) {
56+
// See if title needs to be added
57+
const titles = await db.prepare("all", `SELECT "title", "id" from "casualVoteTitles" WHERE "videoID" = ? AND "service" = ? ORDER BY "id"`, [videoID, service]) as { title: string, id: number }[];
58+
if (titles.length > 0) {
59+
const existingTitle = titles.find((t) => t.title === title);
60+
if (existingTitle) {
61+
titleID = existingTitle.id;
62+
} else {
63+
titleID = titles[titles.length - 1].id + 1;
64+
await db.prepare("run", `INSERT INTO "casualVoteTitles" ("videoID", "service", "hashedVideoID", "id", "title") VALUES (?, ?, ?, ?, ?)`, [videoID, service, hashedVideoID, titleID, title]);
65+
}
66+
} else {
67+
await db.prepare("run", `INSERT INTO "casualVoteTitles" ("videoID", "service", "hashedVideoID", "id", "title") VALUES (?, ?, ?, ?, ?)`, [videoID, service, hashedVideoID, titleID, title]);
68+
}
69+
} else {
70+
const titles = await db.prepare("all", `SELECT "title", "id" from "casualVoteTitles" WHERE "videoID" = ? AND "service" = ? ORDER BY "id"`, [videoID, service]) as { title: string, id: number }[];
71+
if (titles.length > 0) {
72+
titleID = titles[titles.length - 1].id;
73+
}
74+
}
75+
5376
const now = Date.now();
5477
for (const category of categories) {
55-
const existingUUID = (await db.prepare("get", `SELECT "UUID" from "casualVotes" where "videoID" = ? AND "category" = ?`, [videoID, category]))?.UUID;
78+
const existingUUID = (await db.prepare("get", `SELECT "UUID" from "casualVotes" where "videoID" = ? AND "service" = ? AND "titleID" = ? AND "category" = ?`, [videoID, service, titleID, category]))?.UUID;
5679
const UUID = existingUUID || crypto.randomUUID();
5780

58-
const alreadyVotedTheSame = await handleExistingVotes(videoID, service, UUID, hashedUserID, hashedIP, category, downvote, now);
81+
const alreadyVotedTheSame = await handleExistingVotes(videoID, service, titleID, hashedUserID, hashedIP, category, downvote, now);
5982
if (existingUUID) {
6083
if (!alreadyVotedTheSame) {
6184
await db.prepare("run", `UPDATE "casualVotes" SET "upvotes" = "upvotes" + 1 WHERE "UUID" = ?`, [UUID]);
6285
}
6386
} else {
64-
await db.prepare("run", `INSERT INTO "casualVotes" ("videoID", "service", "hashedVideoID", "timeSubmitted", "UUID", "category", "upvotes") VALUES (?, ?, ?, ?, ?, ?, ?)`,
65-
[videoID, service, hashedVideoID, now, UUID, category, 1]);
87+
await db.prepare("run", `INSERT INTO "casualVotes" ("videoID", "service", "titleID", "hashedVideoID", "timeSubmitted", "UUID", "category", "upvotes") VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
88+
[videoID, service, titleID, hashedVideoID, now, UUID, category, 1]);
6689
}
6790
}
6891

@@ -77,31 +100,31 @@ export async function postCasual(req: Request, res: Response) {
77100
}
78101
}
79102

80-
async function handleExistingVotes(videoID: VideoID, service: Service, UUID: string,
103+
async function handleExistingVotes(videoID: VideoID, service: Service, titleID: number,
81104
hashedUserID: HashedUserID, hashedIP: HashedIP, category: CasualCategory, downvote: boolean, now: number): Promise<boolean> {
82-
const existingVote = await privateDB.prepare("get", `SELECT "UUID" from "casualVotes" WHERE "videoID" = ? AND "service" = ? AND "userID" = ? AND "category" = ?`, [videoID, service, hashedUserID, category]) as ExistingVote;
105+
const existingVote = await privateDB.prepare("get", `SELECT "UUID" from "casualVotes" WHERE "videoID" = ? AND "service" = ? AND "titleID" = ? AND "userID" = ? AND "category" = ?`, [videoID, service, titleID, hashedUserID, category]) as ExistingVote;
83106
if (existingVote) {
84107
return true;
85108
} else {
86109
if (downvote) {
87110
// Remove upvotes for all categories on this video
88-
const existingUpvotes = await privateDB.prepare("all", `SELECT "category" from "casualVotes" WHERE "category" != 'downvote' AND "videoID" = ? AND "service" = ? AND "userID" = ?`, [videoID, service, hashedUserID]);
111+
const existingUpvotes = await privateDB.prepare("all", `SELECT "category" from "casualVotes" WHERE "category" != 'downvote' AND "videoID" = ? AND "service" = ? AND "titleID" = ? AND "userID" = ?`, [videoID, service, titleID, hashedUserID]);
89112
for (const existingUpvote of existingUpvotes) {
90-
await db.prepare("run", `UPDATE "casualVotes" SET "upvotes" = "upvotes" - 1 WHERE "videoID" = ? AND "category" = ?`, [videoID, existingUpvote.category]);
91-
await privateDB.prepare("run", `DELETE FROM "casualVotes" WHERE "videoID" = ? AND "userID" = ? AND "category" = ?`, [videoID, hashedUserID, existingUpvote.category]);
113+
await db.prepare("run", `UPDATE "casualVotes" SET "upvotes" = "upvotes" - 1 WHERE "videoID" = ? AND "service" = ? AND "titleID" = ? AND "category" = ?`, [videoID, service, titleID, existingUpvote.category]);
114+
await privateDB.prepare("run", `DELETE FROM "casualVotes" WHERE "videoID" = ? AND "service" = ? AND "titleID" = ? AND "userID" = ? AND "category" = ?`, [videoID, service, titleID, hashedUserID, existingUpvote.category]);
92115
}
93116
} else {
94117
// Undo a downvote if it exists
95-
const existingDownvote = await privateDB.prepare("get", `SELECT "UUID" from "casualVotes" WHERE "category" = 'downvote' AND "videoID" = ? AND "service" = ? AND "userID" = ?`, [videoID, service, hashedUserID]) as ExistingVote;
118+
const existingDownvote = await privateDB.prepare("get", `SELECT "UUID" from "casualVotes" WHERE "category" = 'downvote' AND "videoID" = ? AND "service" = ? AND "titleID" = ? AND "userID" = ?`, [videoID, service, titleID, hashedUserID]) as ExistingVote;
96119
if (existingDownvote) {
97-
await db.prepare("run", `UPDATE "casualVotes" SET "upvotes" = "upvotes" - 1 WHERE "category" = 'downvote' AND "videoID" = ?`, [videoID]);
98-
await privateDB.prepare("run", `DELETE FROM "casualVotes" WHERE "category" = 'downvote' AND "videoID" = ? AND "userID" = ?`, [videoID, hashedUserID]);
120+
await db.prepare("run", `UPDATE "casualVotes" SET "upvotes" = "upvotes" - 1 WHERE "category" = 'downvote' AND "videoID" = ? AND "service" = ? AND "titleID" = ?`, [videoID, service, titleID]);
121+
await privateDB.prepare("run", `DELETE FROM "casualVotes" WHERE "category" = 'downvote' AND "videoID" = ? AND "service" = ? AND "titleID" = ? AND "userID" = ?`, [videoID, service, titleID, hashedUserID]);
99122
}
100123
}
101124
}
102125

103-
await privateDB.prepare("run", `INSERT INTO "casualVotes" ("videoID", "service", "userID", "hashedIP", "category", "timeSubmitted") VALUES (?, ?, ?, ?, ?, ?)`,
104-
[videoID, service, hashedUserID, hashedIP, category, now]);
126+
await privateDB.prepare("run", `INSERT INTO "casualVotes" ("videoID", "service", "titleID", "userID", "hashedIP", "category", "timeSubmitted") VALUES (?, ?, ?, ?, ?, ?, ?)`,
127+
[videoID, service, titleID, hashedUserID, hashedIP, category, now]);
105128

106129
return false;
107130
}

src/types/branding.model.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ export interface ThumbnailResult {
5454

5555
export interface CasualVote {
5656
id: string,
57-
count: number
57+
count: number,
58+
title: string | null
5859
}
5960

6061
export interface BrandingResult {
@@ -107,6 +108,7 @@ export interface CasualVoteSubmission {
107108
service: Service;
108109
downvote: boolean | undefined;
109110
categories: CasualCategory[];
111+
title?: string;
110112
}
111113

112114
export interface BrandingSegmentDBResult {
@@ -120,6 +122,7 @@ export interface CasualVoteDBResult {
120122
category: CasualCategory;
121123
upvotes: number;
122124
downvotes: number;
125+
title?: string;
123126
}
124127

125128
export interface BrandingSegmentHashDBResult extends BrandingDBSubmissionData {

test/cases/getBranding.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ describe("getBranding", () => {
1616
const videoIDvidDuration = "videoID7";
1717
const videoIDCasual = "videoIDCasual";
1818
const videoIDCasualDownvoted = "videoIDCasualDownvoted";
19+
const videoIDCasualTitle = "videoIDCasualTitle";
1920

2021
const videoID1Hash = getHash(videoID1, 1).slice(0, 4);
2122
const videoID2LockedHash = getHash(videoID2Locked, 1).slice(0, 4);
@@ -26,6 +27,7 @@ describe("getBranding", () => {
2627
const videoIDvidDurationHash = getHash(videoIDUnverified, 1).slice(0, 4);
2728
const videoIDCasualHash = getHash(videoIDCasual, 1).slice(0, 4);
2829
const videoIDCasualDownvotedHash = getHash(videoIDCasualDownvoted, 1).slice(0, 4);
30+
const videoIDCasualTitleHash = getHash(videoIDCasualTitle, 1).slice(0, 4);
2931

3032
const endpoint = "/api/branding";
3133
const getBranding = (params: Record<string, any>) => client({
@@ -48,6 +50,7 @@ describe("getBranding", () => {
4850
const thumbnailVotesQuery = `INSERT INTO "thumbnailVotes" ("UUID", "votes", "locked", "shadowHidden", "downvotes", "removed") VALUES (?, ?, ?, ?, ?, ?)`;
4951
const segmentQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "timeSubmitted", "views", "category", "actionType", "service", "videoDuration", "hidden", "shadowHidden", "description", "hashedVideoID") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
5052
const insertCasualVotesQuery = `INSERT INTO "casualVotes" ("UUID", "videoID", "service", "hashedVideoID", "category", "upvotes", "timeSubmitted") VALUES (?, ?, ?, ?, ?, ?, ?)`;
53+
const insertCasualVotesTitleQuery = `INSERT INTO "casualVoteTitles" ("videoID", "service", "hashedVideoID", "id", "title") VALUES (?, ?, ?, ?, ?)`;
5154

5255
await Promise.all([
5356
db.prepare("run", titleQuery, [videoID1, "title1", 0, "userID1", Service.YouTube, videoID1Hash, 1, "UUID1"]),
@@ -154,6 +157,13 @@ describe("getBranding", () => {
154157
db.prepare("run", insertCasualVotesQuery, ["postBrandCasual2", videoIDCasualDownvoted, Service.YouTube, videoIDCasualDownvotedHash, "clever", 1, Date.now()]),
155158
db.prepare("run", insertCasualVotesQuery, ["postBrandCasual2d", videoIDCasualDownvoted, Service.YouTube, videoIDCasualDownvotedHash, "downvote", 1, Date.now()]),
156159
db.prepare("run", insertCasualVotesQuery, ["postBrandCasual3", videoIDCasualDownvoted, Service.YouTube, videoIDCasualDownvotedHash, "other", 4, Date.now()]),
160+
db.prepare("run", insertCasualVotesQuery, ["postBrandCasual4", videoIDCasualTitle, Service.YouTube, videoIDCasualTitleHash, "clever", 8, Date.now()]),
161+
db.prepare("run", insertCasualVotesQuery, ["postBrandCasual4d", videoIDCasualTitle, Service.YouTube, videoIDCasualTitleHash, "downvote", 4, Date.now()]),
162+
db.prepare("run", insertCasualVotesQuery, ["postBrandCasual4o", videoIDCasualTitle, Service.YouTube, videoIDCasualTitleHash, "other", 3, Date.now()]),
163+
]);
164+
165+
await Promise.all([
166+
db.prepare("run", insertCasualVotesTitleQuery, [videoIDCasualTitle, Service.YouTube, videoIDCasualTitleHash, 0, "a cool title"]),
157167
]);
158168
});
159169

@@ -351,7 +361,8 @@ describe("getBranding", () => {
351361
await checkVideo(videoIDCasual, videoIDCasualHash, true, {
352362
casualVotes: [{
353363
id: "clever",
354-
count: 1
364+
count: 1,
365+
title: null
355366
}]
356367
});
357368
});
@@ -360,7 +371,18 @@ describe("getBranding", () => {
360371
await checkVideo(videoIDCasualDownvoted, videoIDCasualDownvotedHash, true, {
361372
casualVotes: [{
362373
id: "other",
363-
count: 3
374+
count: 3,
375+
title: null
376+
}]
377+
});
378+
});
379+
380+
it("should get casual votes with title", async () => {
381+
await checkVideo(videoIDCasualTitle, videoIDCasualTitleHash, true, {
382+
casualVotes: [{
383+
id: "clever",
384+
count: 4,
385+
title: "a cool title"
364386
}]
365387
});
366388
});

test/cases/postCasual.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ describe("postCasual", () => {
1616
data
1717
});
1818

19-
const queryCasualVotesByVideo = (videoID: string, all = false) => db.prepare(all ? "all" : "get", `SELECT * FROM "casualVotes" WHERE "videoID" = ? ORDER BY "timeSubmitted" ASC`, [videoID]);
19+
const queryCasualVotesByVideo = (videoID: string, all = false, titleID = 0) => db.prepare(all ? "all" : "get", `SELECT * FROM "casualVotes" WHERE "videoID" = ? AND "titleID" = ? ORDER BY "timeSubmitted" ASC`, [videoID, titleID]);
2020

2121
it("submit casual vote", async () => {
2222
const videoID = "postCasual1";
@@ -25,6 +25,7 @@ describe("postCasual", () => {
2525
categories: ["clever"],
2626
userID: userID1,
2727
service: Service.YouTube,
28+
title: "title",
2829
videoID
2930
});
3031

@@ -42,6 +43,7 @@ describe("postCasual", () => {
4243
categories: ["clever"],
4344
userID: userID1,
4445
service: Service.YouTube,
46+
title: "title",
4547
videoID
4648
});
4749

@@ -59,6 +61,7 @@ describe("postCasual", () => {
5961
categories: ["clever"],
6062
userID: userID2,
6163
service: Service.YouTube,
64+
title: "title",
6265
videoID
6366
});
6467

@@ -96,6 +99,7 @@ describe("postCasual", () => {
9699
downvote: true,
97100
userID: userID3,
98101
service: Service.YouTube,
102+
title: "title",
99103
videoID
100104
});
101105

@@ -117,6 +121,7 @@ describe("postCasual", () => {
117121
downvote: false,
118122
userID: userID3,
119123
service: Service.YouTube,
124+
title: "title",
120125
videoID
121126
});
122127

@@ -137,6 +142,7 @@ describe("postCasual", () => {
137142
categories: ["clever", "other"],
138143
userID: userID1,
139144
service: Service.YouTube,
145+
title: "title",
140146
videoID
141147
});
142148

@@ -157,6 +163,7 @@ describe("postCasual", () => {
157163
downvote: true,
158164
userID: userID1,
159165
service: Service.YouTube,
166+
title: "title",
160167
videoID
161168
});
162169

@@ -180,6 +187,7 @@ describe("postCasual", () => {
180187
categories: ["clever", "other"],
181188
userID: userID1,
182189
service: Service.YouTube,
190+
title: "title",
183191
videoID
184192
});
185193

@@ -199,6 +207,7 @@ describe("postCasual", () => {
199207
const res = await postCasual({
200208
userID: userID1,
201209
service: Service.YouTube,
210+
title: "title",
202211
videoID,
203212
downvote: true
204213
});
@@ -210,4 +219,33 @@ describe("postCasual", () => {
210219
assert.strictEqual(dbVotes.upvotes, 1);
211220
});
212221

222+
it("submit multiple casual votes for different title", async () => {
223+
const videoID = "postCasual2";
224+
225+
const res = await postCasual({
226+
categories: ["clever", "funny"],
227+
userID: userID2,
228+
service: Service.YouTube,
229+
title: "title 2",
230+
videoID
231+
});
232+
233+
assert.strictEqual(res.status, 200);
234+
const dbVotes = await queryCasualVotesByVideo(videoID, true, 1);
235+
236+
assert.strictEqual(dbVotes[0].category, "clever");
237+
assert.strictEqual(dbVotes[0].upvotes, 1);
238+
239+
assert.strictEqual(dbVotes[1].category, "funny");
240+
assert.strictEqual(dbVotes[1].upvotes, 1);
241+
242+
const dbVotesOriginal = await queryCasualVotesByVideo(videoID, true, 0);
243+
244+
assert.strictEqual(dbVotesOriginal[0].category, "clever");
245+
assert.strictEqual(dbVotesOriginal[0].upvotes, 1);
246+
247+
assert.strictEqual(dbVotesOriginal[1].category, "other");
248+
assert.strictEqual(dbVotesOriginal[1].upvotes, 1);
249+
});
250+
213251
});

test/utils/partialDeepEquals.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export const partialDeepEquals = (actual: Record<string, any>, expected: Record<
1010
// loop over key, value of expected
1111
for (const [key, value] of Object.entries(expected)) {
1212
// if value is object or array, recurse
13-
if (Array.isArray(value) || typeof value === "object") {
13+
if (Array.isArray(value) || (typeof value === "object" && value !== null)) {
1414
if (!partialDeepEquals(actual?.[key], value, false)) {
1515
if (print) printActualExpected(actual, expected, key);
1616
return false;

0 commit comments

Comments
 (0)