Date: Mon, 22 Mar 2021 22:18:23 +0100
Subject: [PATCH 12/32] Timestamp based dump filenames and garbage collection
---
config.json.example | 3 +-
src/config.ts | 3 +-
src/routes/dumpDatabase.ts | 120 ++++++++++++++++++++++++++++++++++---
src/types/config.model.ts | 3 +-
4 files changed, 119 insertions(+), 10 deletions(-)
diff --git a/config.json.example b/config.json.example
index 65f9b42..580b8fb 100644
--- a/config.json.example
+++ b/config.json.example
@@ -42,7 +42,8 @@
"dumpDatabase": {
"enabled": true,
"minTimeBetweenMs": 60000, // 1 minute between dumps
- "exportPath": "/opt/exports",
+ "appExportPath": "/opt/exports",
+ "postgresExportPath": "/opt/exports",
"tables": [{
"name": "sponsorTimes",
"order": "timeSubmitted"
diff --git a/src/config.ts b/src/config.ts
index 477aa72..0e985dc 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -49,7 +49,8 @@ addDefaults(config, {
dumpDatabase: {
enabled: true,
minTimeBetweenMs: 60000,
- exportPath: '/opt/exports',
+ appExportPath: '/opt/exports',
+ postgresExportPath: '/opt/exports',
tables: [{
name: "sponsorTimes",
order: "timeSubmitted"
diff --git a/src/routes/dumpDatabase.ts b/src/routes/dumpDatabase.ts
index ec3e801..b7f2726 100644
--- a/src/routes/dumpDatabase.ts
+++ b/src/routes/dumpDatabase.ts
@@ -2,10 +2,29 @@ import {db} from '../databases/databases';
import {Logger} from '../utils/logger';
import {Request, Response} from 'express';
import { config } from '../config';
+const util = require('util');
+const fs = require('fs');
+const path = require('path');
+const unlink = util.promisify(fs.unlink);
+const fstat = util.promisify(fs.fstat);
const ONE_MINUTE = 1000 * 60;
-const styleHeader = ``
+const styleHeader = ``
const licenseHeader = `The API and database follow CC BY-NC-SA 4.0 unless you have explicit permission.
Attribution Template
@@ -13,7 +32,15 @@ const licenseHeader = `The API and database follow {
+ return new Promise((resolve, reject) => {
+ // Get list of table names
+ // Create array for each table
+ const tableFiles = tableNames.reduce((obj: any, tableName) => {
+ obj[tableName] = [];
+ return obj;
+ }, {});
+ // read files in export directory
+ fs.readdir(exportPath, (err: any, files: string[]) => {
+ if (err) Logger.error(err);
+ if (err) return resolve();
+ files.forEach(file => {
+ // we only care about files that start with "_" and ends with .csv
+ tableNames.forEach(tableName => {
+ if (file.startsWith(`${tableName}_`) && file.endsWith('.csv')) {
+ // extract the timestamp from the filename
+ // we could also use the fs.stat mtime
+ const timestamp = Number(file.split('_')[1].replace('.csv', ''));
+ tableFiles[tableName].push({
+ file: path.join(exportPath, file),
+ timestamp,
+ });
+ }
+ });
+ });
+ const outdatedTime = Math.floor(Date.now() - (MILLISECONDS_BETWEEN_DUMPS * 1.5));
+ for (let tableName in tableFiles) {
+ const files = tableFiles[tableName];
+ files.forEach(async (item: any) => {
+ if (item.timestamp < outdatedTime) {
+ // remove old file
+ await unlink(item.file).catch((error: any) => {
+ Logger.error(`[dumpDatabase] Garbage collection failed ${error}`);
+ });
+ }
+ });
+ }
+ resolve();
+ });
+ });
+}
+
+export default async function dumpDatabase(req: Request, res: Response, showPage: boolean) {
if (config?.dumpDatabase?.enabled === false) {
res.status(404).send("Database dump is disabled");
return;
@@ -48,22 +118,58 @@ export default function dumpDatabase(req: Request, res: Response, showPage: bool
Send a request to https://sponsor.ajay.app/database.json, or visit this page to trigger the database dump to run.
Then, you can download the csv files below, or use the links returned from the JSON request.
Links
- ${linksHTML}
+
+
+
+ | Table |
+ CSV |
+
+
+
+ ${latestDumpFiles.map((item:any) => {
+ return `
+
+ | ${item.tableName} |
+ ${item.fileName} |
+
+ `;
+ }).join('')}
+ ${latestDumpFiles.length === 0 ? '| Please wait: Generating files |
' : ''}
+
+
+
${updateQueued ? `Update queued.` : ``} Last updated: ${lastUpdate ? new Date(lastUpdate).toUTCString() : `Unknown`}`);
} else {
res.send({
lastUpdated: lastUpdate,
updateQueued,
- links
+ links: latestDumpFiles.map((item:any) => {
+ return {
+ table: item.tableName,
+ url: `/download/${item.fileName}`,
+ size: item.fileSize,
+ };
+ }),
})
}
if (updateQueued) {
lastUpdate = Date.now();
+
+ await removeOutdatedDumps(appExportPath);
+
+ const dumpFiles = [];
for (const table of tables) {
- db.prepare('run', `COPY (SELECT * FROM "${table.name}"${table.order ? ` ORDER BY "${table.order}"` : ``})
- TO '${exportPath}/${table.name}.csv' WITH (FORMAT CSV, HEADER true);`);
+ const fileName = `${table.name}_${lastUpdate}.csv`;
+ const file = `${postgresExportPath}/${fileName}`;
+ await db.prepare('run', `COPY (SELECT * FROM "${table.name}"${table.order ? ` ORDER BY "${table.order}"` : ``})
+ TO '${file}' WITH (FORMAT CSV, HEADER true);`);
+ dumpFiles.push({
+ fileName,
+ tableName: table.name,
+ });
}
+ latestDumpFiles = [...dumpFiles];
}
}
diff --git a/src/types/config.model.ts b/src/types/config.model.ts
index c0611b7..f46cc17 100644
--- a/src/types/config.model.ts
+++ b/src/types/config.model.ts
@@ -67,7 +67,8 @@ export interface PostgresConfig {
export interface DumpDatabase {
enabled: boolean;
minTimeBetweenMs: number;
- exportPath: string;
+ appExportPath: string;
+ postgresExportPath: string;
tables: DumpDatabaseTable[];
}
From 27c2562a7f7319c122bbbf68e6196f361635a037 Mon Sep 17 00:00:00 2001
From: Ajay Ramachandran
Date: Tue, 23 Mar 2021 23:46:46 -0400
Subject: [PATCH 13/32] Add more indexes
---
databases/_private_indexes.sql | 10 +++++-----
databases/_sponsorTimes_indexes.sql | 8 ++++----
2 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/databases/_private_indexes.sql b/databases/_private_indexes.sql
index d7fb746..2b4781e 100644
--- a/databases/_private_indexes.sql
+++ b/databases/_private_indexes.sql
@@ -1,15 +1,15 @@
-- sponsorTimes
-CREATE INDEX IF NOT EXISTS "idx_16928_sponsorTimes_hashedIP"
- ON public."sponsorTimes" USING btree
- ("hashedIP" COLLATE pg_catalog."default" ASC NULLS LAST)
- TABLESPACE pg_default;
-
CREATE INDEX IF NOT EXISTS "sponsorTimes_hashedIP"
ON public."sponsorTimes" USING btree
("hashedIP" COLLATE pg_catalog."default" ASC NULLS LAST)
TABLESPACE pg_default;
+CREATE INDEX "privateDB_sponsorTimes_videoID"
+ ON public."sponsorTimes" USING btree
+ ("videoID" ASC NULLS LAST)
+;
+
-- votes
CREATE INDEX IF NOT EXISTS "votes_userID"
diff --git a/databases/_sponsorTimes_indexes.sql b/databases/_sponsorTimes_indexes.sql
index 33f1913..6d4d91d 100644
--- a/databases/_sponsorTimes_indexes.sql
+++ b/databases/_sponsorTimes_indexes.sql
@@ -14,10 +14,10 @@ CREATE INDEX IF NOT EXISTS "sponsorTimes_UUID"
ON public."sponsorTimes" USING btree
("UUID" COLLATE pg_catalog."default" ASC NULLS LAST)
TABLESPACE pg_default;
-
-CREATE INDEX IF NOT EXISTS "sponsorTimes_hashedVideoID"
- ON public."sponsorTimes" USING btree
- ("hashedVideoID" COLLATE pg_catalog."default" ASC NULLS LAST, category COLLATE pg_catalog."default" ASC NULLS LAST, "startTime" ASC NULLS LAST)
+
+CREATE INDEX "sponsorTimes_hashedVideoID_gin"
+ ON public."sponsorTimes" USING gin
+ ("hashedVideoID" COLLATE pg_catalog."default" gin_trgm_ops, category COLLATE pg_catalog."default" gin_trgm_ops)
TABLESPACE pg_default;
CREATE INDEX IF NOT EXISTS "sponsorTimes_videoID"
From 5c827baa1adc2d2d9f568a740c5167bdfce9055f Mon Sep 17 00:00:00 2001
From: Ajay Ramachandran
Date: Thu, 25 Mar 2021 18:48:44 -0400
Subject: [PATCH 14/32] Update db link
---
README.MD | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.MD b/README.MD
index 8f61c57..e309e54 100644
--- a/README.MD
+++ b/README.MD
@@ -8,7 +8,7 @@ This is the server backend for it
This uses a Postgres or Sqlite database to hold all the timing data.
-To make sure that this project doesn't die, I have made the database publicly downloadable at https://sponsor.ajay.app/database.db. You can download a backup or get archive.org to take a backup if you do desire. The database is under [this license](https://creativecommons.org/licenses/by-nc-sa/4.0/) unless you get explicit permission from me.
+To make sure that this project doesn't die, I have made the database publicly downloadable at https://sponsor.ajay.app/database. You can download a backup or get archive.org to take a backup if you do desire. The database is under [this license](https://creativecommons.org/licenses/by-nc-sa/4.0/) unless you get explicit permission from me.
Hopefully this project can be combined with projects like [this](https://github.com/Sponsoff/sponsorship_remover) and use this data to create a neural network to predict when sponsored segments happen. That project is sadly abandoned now, so I have decided to attempt to revive this idea.
From c7eb5fed35fa520ae35d8fdef56a4e76ffa9ed2a Mon Sep 17 00:00:00 2001
From: Ajay Ramachandran
Date: Fri, 26 Mar 2021 18:35:25 -0400
Subject: [PATCH 15/32] Fix video duration precision and use submitted one when
possible
---
databases/_upgrade_sponsorTimes_9.sql | 29 +++++++++++++++++++++++++++
src/routes/postSkipSegments.ts | 6 +++++-
2 files changed, 34 insertions(+), 1 deletion(-)
create mode 100644 databases/_upgrade_sponsorTimes_9.sql
diff --git a/databases/_upgrade_sponsorTimes_9.sql b/databases/_upgrade_sponsorTimes_9.sql
new file mode 100644
index 0000000..5015a4a
--- /dev/null
+++ b/databases/_upgrade_sponsorTimes_9.sql
@@ -0,0 +1,29 @@
+BEGIN TRANSACTION;
+
+/* Add Service field */
+CREATE TABLE "sqlb_temp_table_9" (
+ "videoID" TEXT NOT NULL,
+ "startTime" REAL NOT NULL,
+ "endTime" REAL NOT NULL,
+ "votes" INTEGER NOT NULL,
+ "locked" INTEGER NOT NULL default '0',
+ "incorrectVotes" INTEGER NOT NULL default '1',
+ "UUID" TEXT NOT NULL UNIQUE,
+ "userID" TEXT NOT NULL,
+ "timeSubmitted" INTEGER NOT NULL,
+ "views" INTEGER NOT NULL,
+ "category" TEXT NOT NULL DEFAULT 'sponsor',
+ "service" TEXT NOT NULL DEFAULT 'YouTube',
+ "videoDuration" REAL NOT NULL DEFAULT '0',
+ "shadowHidden" INTEGER NOT NULL,
+ "hashedVideoID" TEXT NOT NULL default ''
+);
+
+INSERT INTO sqlb_temp_table_9 SELECT "videoID","startTime","endTime","votes","locked","incorrectVotes","UUID","userID","timeSubmitted","views","category","service",'0', "shadowHidden","hashedVideoID" FROM "sponsorTimes";
+
+DROP TABLE "sponsorTimes";
+ALTER TABLE sqlb_temp_table_9 RENAME TO "sponsorTimes";
+
+UPDATE "config" SET value = 9 WHERE key = 'version';
+
+COMMIT;
\ No newline at end of file
diff --git a/src/routes/postSkipSegments.ts b/src/routes/postSkipSegments.ts
index 3b6f82d..2f847bf 100644
--- a/src/routes/postSkipSegments.ts
+++ b/src/routes/postSkipSegments.ts
@@ -423,7 +423,11 @@ export async function postSkipSegments(req: Request, res: Response) {
if (service == Service.YouTube) {
apiVideoInfo = await getYouTubeVideoInfo(videoID);
}
- videoDuration = getYouTubeVideoDuration(apiVideoInfo) || videoDuration;
+ const apiVideoDuration = getYouTubeVideoDuration(apiVideoInfo);
+ if (!apiVideoDuration || Math.abs(videoDuration - apiVideoDuration) > 2) {
+ // If api duration is far off, take that one instead (it is only precise to seconds, not millis)
+ videoDuration = apiVideoDuration;
+ }
// Auto moderator check
if (!isVIP && service == Service.YouTube) {
From 46524e4298e3536a534c90173043290cad213993 Mon Sep 17 00:00:00 2001
From: Ajay Ramachandran
Date: Fri, 26 Mar 2021 19:02:32 -0400
Subject: [PATCH 16/32] Fix indexes
---
databases/_private_indexes.sql | 2 +-
databases/_sponsorTimes_indexes.sql | 8 ++++----
src/databases/Postgres.ts | 7 ++++++-
3 files changed, 11 insertions(+), 6 deletions(-)
diff --git a/databases/_private_indexes.sql b/databases/_private_indexes.sql
index 2b4781e..2b70fa7 100644
--- a/databases/_private_indexes.sql
+++ b/databases/_private_indexes.sql
@@ -5,7 +5,7 @@ CREATE INDEX IF NOT EXISTS "sponsorTimes_hashedIP"
("hashedIP" COLLATE pg_catalog."default" ASC NULLS LAST)
TABLESPACE pg_default;
-CREATE INDEX "privateDB_sponsorTimes_videoID"
+CREATE INDEX IF NOT EXISTS "privateDB_sponsorTimes_videoID"
ON public."sponsorTimes" USING btree
("videoID" ASC NULLS LAST)
;
diff --git a/databases/_sponsorTimes_indexes.sql b/databases/_sponsorTimes_indexes.sql
index 6d4d91d..c90ef51 100644
--- a/databases/_sponsorTimes_indexes.sql
+++ b/databases/_sponsorTimes_indexes.sql
@@ -1,6 +1,6 @@
-- sponsorTimes
-CREATE INDEX IF NOT EXISTS "sponsorTiems_timeSubmitted"
+CREATE INDEX IF NOT EXISTS "sponsorTime_timeSubmitted"
ON public."sponsorTimes" USING btree
("timeSubmitted" ASC NULLS LAST)
TABLESPACE pg_default;
@@ -14,8 +14,8 @@ CREATE INDEX IF NOT EXISTS "sponsorTimes_UUID"
ON public."sponsorTimes" USING btree
("UUID" COLLATE pg_catalog."default" ASC NULLS LAST)
TABLESPACE pg_default;
-
-CREATE INDEX "sponsorTimes_hashedVideoID_gin"
+
+CREATE INDEX IF NOT EXISTS "sponsorTimes_hashedVideoID_gin"
ON public."sponsorTimes" USING gin
("hashedVideoID" COLLATE pg_catalog."default" gin_trgm_ops, category COLLATE pg_catalog."default" gin_trgm_ops)
TABLESPACE pg_default;
@@ -41,7 +41,7 @@ CREATE INDEX IF NOT EXISTS "vipUsers_index"
-- warnings
-CREATE INDEX IF NOT EXISTS warnings_index
+CREATE INDEX IF NOT EXISTS "warnings_index"
ON public.warnings USING btree
("userID" COLLATE pg_catalog."default" ASC NULLS LAST, "issueTime" DESC NULLS LAST, enabled DESC NULLS LAST)
TABLESPACE pg_default;
diff --git a/src/databases/Postgres.ts b/src/databases/Postgres.ts
index 1f87e8f..e7e16f4 100644
--- a/src/databases/Postgres.ts
+++ b/src/databases/Postgres.ts
@@ -24,7 +24,12 @@ export class Postgres implements IDatabase {
// Upgrade database if required
await this.upgradeDB(this.config.fileNamePrefix, this.config.dbSchemaFolder);
- await this.applyIndexes(this.config.fileNamePrefix, this.config.dbSchemaFolder);
+ try {
+ await this.applyIndexes(this.config.fileNamePrefix, this.config.dbSchemaFolder);
+ } catch (e) {
+ Logger.warn("Applying indexes failed. See https://github.com/ajayyy/SponsorBlockServer/wiki/Postgres-Extensions for more information.");
+ Logger.warn(e);
+ }
}
}
From 37a07ace72d13f18c281b25f29c2c2c3fd27cd86 Mon Sep 17 00:00:00 2001
From: Ajay Ramachandran
Date: Fri, 26 Mar 2021 19:03:30 -0400
Subject: [PATCH 17/32] Cache data for getting hash-prefix segments
---
src/middleware/redisKeys.ts | 10 ++++++-
src/routes/getSkipSegments.ts | 43 ++++++++++++++++++++++++------
src/routes/postSkipSegments.ts | 6 +++--
src/routes/voteOnSponsorTime.ts | 47 ++++++++++++++++++++-------------
4 files changed, 76 insertions(+), 30 deletions(-)
diff --git a/src/middleware/redisKeys.ts b/src/middleware/redisKeys.ts
index 1335e15..56b1aab 100644
--- a/src/middleware/redisKeys.ts
+++ b/src/middleware/redisKeys.ts
@@ -1,5 +1,13 @@
-import { Category, VideoID } from "../types/segments.model";
+import { Service, VideoID, VideoIDHash } from "../types/segments.model";
+import { Logger } from "../utils/logger";
export function skipSegmentsKey(videoID: VideoID): string {
return "segments-" + videoID;
}
+
+export function skipSegmentsHashKey(hashedVideoIDPrefix: VideoIDHash, service: Service): string {
+ hashedVideoIDPrefix = hashedVideoIDPrefix.substring(0, 4) as VideoIDHash;
+ if (hashedVideoIDPrefix.length !== 4) Logger.warn("Redis skip segment hash-prefix key is not length 4! " + hashedVideoIDPrefix);
+
+ return "segments." + service + "." + hashedVideoIDPrefix;
+}
\ No newline at end of file
diff --git a/src/routes/getSkipSegments.ts b/src/routes/getSkipSegments.ts
index b6ae72f..c2a9155 100644
--- a/src/routes/getSkipSegments.ts
+++ b/src/routes/getSkipSegments.ts
@@ -2,7 +2,7 @@ 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 { skipSegmentsHashKey, skipSegmentsKey } from '../middleware/redisKeys';
import { SBRecord } from '../types/lib.model';
import { Category, DBSegment, HashedIP, IPAddress, OverlappingSegmentGroup, Segment, SegmentCache, Service, VideoData, VideoID, VideoIDHash, Visibility, VotableObject } from "../types/segments.model";
import { getHash } from '../utils/getHash';
@@ -92,13 +92,9 @@ async function getSegmentsByHash(req: Request, hashedVideoIDPrefix: VideoIDHash,
categories = categories.filter((category) => !(/[^a-z|_|-]/.test(category)));
if (categories.length === 0) return null;
- const segmentPerVideoID: SegmentWithHashPerVideoID = (await db
- .prepare(
- 'all',
- `SELECT "videoID", "startTime", "endTime", "votes", "locked", "UUID", "category", "videoDuration", "shadowHidden", "hashedVideoID" FROM "sponsorTimes"
- WHERE "hashedVideoID" LIKE ? AND "category" IN (${categories.map((c) => "'" + c + "'")}) AND "service" = ? ORDER BY "startTime"`,
- [hashedVideoIDPrefix + '%', service]
- )).reduce((acc: SegmentWithHashPerVideoID, segment: DBSegment) => {
+ const segmentPerVideoID: SegmentWithHashPerVideoID = (await getSegmentsFromDB(hashedVideoIDPrefix, service))
+ .filter((segment: DBSegment) => categories.includes(segment?.category))
+ .reduce((acc: SegmentWithHashPerVideoID, segment: DBSegment) => {
acc[segment.videoID] = acc[segment.videoID] || {
hash: segment.hashedVideoID,
segmentPerCategory: {},
@@ -131,6 +127,37 @@ async function getSegmentsByHash(req: Request, hashedVideoIDPrefix: VideoIDHash,
}
}
+async function getSegmentsFromDB(hashedVideoIDPrefix: VideoIDHash, service: Service): Promise {
+ const fetchFromDB = () => db
+ .prepare(
+ 'all',
+ `SELECT "videoID", "startTime", "endTime", "votes", "locked", "UUID", "category", "videoDuration", "shadowHidden", "hashedVideoID" FROM "sponsorTimes"
+ WHERE "hashedVideoID" LIKE ? AND "service" = ? ORDER BY "startTime"`,
+ [hashedVideoIDPrefix + '%', service]
+ );
+
+ if (hashedVideoIDPrefix.length === 4) {
+ const key = skipSegmentsHashKey(hashedVideoIDPrefix, service);
+ const {err, reply} = await redis.getAsync(key);
+
+ 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
+ }
+ }
+
+ const data = await fetchFromDB();
+
+ redis.setAsync(key, JSON.stringify(data));
+ return data;
+ }
+
+ return await fetchFromDB();
+}
+
//gets a weighted random choice from the choices array based on their `votes` property.
//amountOfChoices specifies the maximum amount of choices to return, 1 or more.
//choices are unique
diff --git a/src/routes/postSkipSegments.ts b/src/routes/postSkipSegments.ts
index 2f847bf..75b572d 100644
--- a/src/routes/postSkipSegments.ts
+++ b/src/routes/postSkipSegments.ts
@@ -11,7 +11,7 @@ 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 { skipSegmentsHashKey, skipSegmentsKey } from '../middleware/redisKeys';
import redis from '../utils/redis';
import { Category, IncomingSegment, Segment, Service, VideoDuration, VideoID } from '../types/segments.model';
@@ -497,13 +497,14 @@ export async function postSkipSegments(req: Request, res: Response) {
//it's better than generating an actual UUID like what was used before
//also better for duplication checking
const UUID = getSubmissionUUID(videoID, segmentInfo.category, userID, parseFloat(segmentInfo.segment[0]), parseFloat(segmentInfo.segment[1]));
+ const hashedVideoID = getHash(videoID, 1);
const startingLocked = isVIP ? 1 : 0;
try {
await db.prepare('run', `INSERT INTO "sponsorTimes"
("videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "timeSubmitted", "views", "category", "service", "videoDuration", "shadowHidden", "hashedVideoID")
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
- videoID, segmentInfo.segment[0], segmentInfo.segment[1], startingVotes, startingLocked, UUID, userID, timeSubmitted, 0, segmentInfo.category, service, videoDuration, shadowBanned, getHash(videoID, 1),
+ videoID, segmentInfo.segment[0], segmentInfo.segment[1], startingVotes, startingLocked, UUID, userID, timeSubmitted, 0, segmentInfo.category, service, videoDuration, shadowBanned, hashedVideoID,
],
);
@@ -512,6 +513,7 @@ export async function postSkipSegments(req: Request, res: Response) {
// Clear redis cache for this video
redis.delAsync(skipSegmentsKey(videoID));
+ redis.delAsync(skipSegmentsHashKey(hashedVideoID, service));
} catch (err) {
//a DB change probably occurred
res.sendStatus(500);
diff --git a/src/routes/voteOnSponsorTime.ts b/src/routes/voteOnSponsorTime.ts
index 29f0e66..c0aaac0 100644
--- a/src/routes/voteOnSponsorTime.ts
+++ b/src/routes/voteOnSponsorTime.ts
@@ -12,8 +12,8 @@ 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';
+import { skipSegmentsHashKey, skipSegmentsKey } from '../middleware/redisKeys';
+import { Category, HashedIP, IPAddress, SegmentUUID, Service, VideoID, VideoIDHash } from '../types/segments.model';
const voteTypes = {
normal: 0,
@@ -147,8 +147,8 @@ async function sendWebhooks(voteData: VoteData) {
}
}
-async function categoryVote(UUID: string, userID: string, isVIP: boolean, isOwnSubmission: boolean, category: string
- , hashedIP: string, finalResponse: FinalResponse, res: Response) {
+async function categoryVote(UUID: SegmentUUID, userID: UserID, isVIP: boolean, isOwnSubmission: boolean, category: Category
+ , hashedIP: HashedIP, finalResponse: FinalResponse, res: Response) {
// Check if they've already made a vote
const usersLastVoteInfo = await privateDB.prepare('get', `select count(*) as votes, category from "categoryVotes" where "UUID" = ? and "userID" = ? group by category`, [UUID, userID]);
@@ -158,8 +158,9 @@ async function categoryVote(UUID: string, userID: string, isVIP: boolean, isOwnS
return;
}
- const currentCategory = await db.prepare('get', `select category from "sponsorTimes" where "UUID" = ?`, [UUID]);
- if (!currentCategory) {
+ const videoInfo = (await db.prepare('get', `SELECT "category", "videoID", "hashedVideoID", "service" FROM "sponsorTimes" WHERE "UUID" = ?`,
+ [UUID])) as {category: Category, videoID: VideoID, hashedVideoID: VideoIDHash, service: Service};
+ if (!videoInfo) {
// Submission doesn't exist
res.status(400).send("Submission doesn't exist.");
return;
@@ -196,7 +197,7 @@ async function categoryVote(UUID: string, userID: string, isVIP: boolean, isOwnS
}
// See if the submissions category is ready to change
- const currentCategoryInfo = await db.prepare("get", `select votes from "categoryVotes" where "UUID" = ? and category = ?`, [UUID, currentCategory.category]);
+ const currentCategoryInfo = await db.prepare("get", `select votes from "categoryVotes" where "UUID" = ? and category = ?`, [UUID, videoInfo.category]);
const submissionInfo = await db.prepare("get", `SELECT "userID", "timeSubmitted", "votes" FROM "sponsorTimes" WHERE "UUID" = ?`, [UUID]);
const isSubmissionVIP = submissionInfo && await isUserVIP(submissionInfo.userID);
@@ -208,9 +209,9 @@ async function categoryVote(UUID: string, userID: string, isVIP: boolean, isOwnS
// Add submission as vote
if (!currentCategoryInfo && submissionInfo) {
- await db.prepare("run", `insert into "categoryVotes" ("UUID", "category", "votes") values (?, ?, ?)`, [UUID, currentCategory.category, currentCategoryCount]);
+ await db.prepare("run", `insert into "categoryVotes" ("UUID", "category", "votes") values (?, ?, ?)`, [UUID, videoInfo.category, currentCategoryCount]);
- await privateDB.prepare("run", `insert into "categoryVotes" ("UUID", "userID", "hashedIP", "category", "timeSubmitted") values (?, ?, ?, ?, ?)`, [UUID, submissionInfo.userID, "unknown", currentCategory.category, submissionInfo.timeSubmitted]);
+ await privateDB.prepare("run", `insert into "categoryVotes" ("UUID", "userID", "hashedIP", "category", "timeSubmitted") values (?, ?, ?, ?, ?)`, [UUID, submissionInfo.userID, "unknown", videoInfo.category, submissionInfo.timeSubmitted]);
}
const nextCategoryCount = (nextCategoryInfo?.votes || 0) + voteAmount;
@@ -222,6 +223,8 @@ async function categoryVote(UUID: string, userID: string, isVIP: boolean, isOwnS
await db.prepare('run', `update "sponsorTimes" set "category" = ? where "UUID" = ?`, [category, UUID]);
}
+ clearRedisCache(videoInfo);
+
res.sendStatus(finalResponse.finalStatus);
}
@@ -230,10 +233,10 @@ export function getUserID(req: Request): UserID {
}
export async function voteOnSponsorTime(req: Request, res: Response) {
- const UUID = req.query.UUID as string;
+ const UUID = req.query.UUID as SegmentUUID;
const paramUserID = getUserID(req);
let type = req.query.type !== undefined ? parseInt(req.query.type as string) : undefined;
- const category = req.query.category as string;
+ const category = req.query.category as Category;
if (UUID === undefined || paramUserID === undefined || (type === undefined && category === undefined)) {
//invalid request
@@ -255,7 +258,7 @@ export async function voteOnSponsorTime(req: Request, res: Response) {
const ip = getIP(req);
//hash the ip 5000 times so no one can get it from the database
- const hashedIP = getHash(ip + config.globalSalt);
+ const hashedIP: HashedIP = getHash((ip + config.globalSalt) as IPAddress);
//check if this user is on the vip list
const isVIP = (await db.prepare('get', `SELECT count(*) as "userCount" FROM "vipUsers" WHERE "userID" = ?`, [nonAnonUserID])).userCount > 0;
@@ -350,13 +353,13 @@ 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 = await db.prepare('get', `SELECT "videoID", votes, views FROM "sponsorTimes" WHERE "UUID" = ?`, [UUID]) as
- {videoID: VideoID, votes: number, views: number};
+ const videoInfo = await db.prepare('get', `SELECT "videoID", "hashedVideoID", "service", "votes", "views" FROM "sponsorTimes" WHERE "UUID" = ?`, [UUID]) as
+ {videoID: VideoID, hashedVideoID: VideoIDHash, service: Service, votes: number, views: number};
if (voteTypeEnum === voteTypes.normal) {
if ((isVIP || isOwnSubmission) && incrementAmount < 0) {
//this user is a vip and a downvote
- incrementAmount = -(row.votes + 2 - oldIncrementAmount);
+ incrementAmount = -(videoInfo.votes + 2 - oldIncrementAmount);
type = incrementAmount;
}
} else if (voteTypeEnum == voteTypes.incorrect) {
@@ -399,8 +402,7 @@ export async function voteOnSponsorTime(req: Request, res: Response) {
await db.prepare('run', 'UPDATE "sponsorTimes" SET locked = 0 WHERE "UUID" = ?', [UUID]);
}
- // Clear redis cache for this video
- redis.delAsync(skipSegmentsKey(row?.videoID));
+ clearRedisCache(videoInfo);
//for each positive vote, see if a hidden submission can be shown again
if (incrementAmount > 0 && voteTypeEnum === voteTypes.normal) {
@@ -437,7 +439,7 @@ export async function voteOnSponsorTime(req: Request, res: Response) {
voteTypeEnum,
isVIP,
isOwnSubmission,
- row,
+ row: videoInfo,
category,
incrementAmount,
oldIncrementAmount,
@@ -449,4 +451,11 @@ export async function voteOnSponsorTime(req: Request, res: Response) {
res.status(500).json({error: 'Internal error creating segment vote'});
}
-}
\ No newline at end of file
+}
+
+function clearRedisCache(videoInfo: { videoID: VideoID; hashedVideoID: VideoIDHash; service: Service; }) {
+ if (videoInfo) {
+ redis.delAsync(skipSegmentsKey(videoInfo.videoID));
+ redis.delAsync(skipSegmentsHashKey(videoInfo.hashedVideoID, videoInfo.service));
+ }
+}
From 5152d7e6495b40472bb21c9ba893fc5ec3eb2c08 Mon Sep 17 00:00:00 2001
From: Ajay Ramachandran
Date: Fri, 26 Mar 2021 19:13:52 -0400
Subject: [PATCH 18/32] Fixed tests
---
src/routes/postSkipSegments.ts | 4 ++--
test/cases/postSkipSegments.ts | 36 ++++++++++++++++++++++++++++++++--
2 files changed, 36 insertions(+), 4 deletions(-)
diff --git a/src/routes/postSkipSegments.ts b/src/routes/postSkipSegments.ts
index 75b572d..7738653 100644
--- a/src/routes/postSkipSegments.ts
+++ b/src/routes/postSkipSegments.ts
@@ -424,9 +424,9 @@ export async function postSkipSegments(req: Request, res: Response) {
apiVideoInfo = await getYouTubeVideoInfo(videoID);
}
const apiVideoDuration = getYouTubeVideoDuration(apiVideoInfo);
- if (!apiVideoDuration || Math.abs(videoDuration - apiVideoDuration) > 2) {
+ if (!videoDuration || (apiVideoDuration && Math.abs(videoDuration - apiVideoDuration) > 2)) {
// If api duration is far off, take that one instead (it is only precise to seconds, not millis)
- videoDuration = apiVideoDuration;
+ videoDuration = apiVideoDuration || 0 as VideoDuration;
}
// Auto moderator check
diff --git a/test/cases/postSkipSegments.ts b/test/cases/postSkipSegments.ts
index 9ca8333..399e922 100644
--- a/test/cases/postSkipSegments.ts
+++ b/test/cases/postSkipSegments.ts
@@ -97,7 +97,7 @@ describe('postSkipSegments', () => {
.catch(err => done(err));
});
- it('Should be able to submit a single time with a duration (JSON method)', (done: Done) => {
+ it('Should be able to submit a single time with a duration from the YouTube API (JSON method)', (done: Done) => {
fetch(getbaseURL()
+ "/api/postVideoSponsorTimes", {
method: 'POST',
@@ -129,7 +129,39 @@ describe('postSkipSegments', () => {
.catch(err => done(err));
});
- it('Should be able to submit a single time with a duration from the API (JSON method)', (done: Done) => {
+ it('Should be able to submit a single time with a precise duration close to the one from the YouTube API (JSON method)', (done: Done) => {
+ fetch(getbaseURL()
+ + "/api/postVideoSponsorTimes", {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ userID: "test",
+ videoID: "dQw4w9WgXZH",
+ videoDuration: 5010.20,
+ segments: [{
+ segment: [1, 10],
+ category: "sponsor",
+ }],
+ }),
+ })
+ .then(async res => {
+ if (res.status === 200) {
+ const row = await db.prepare('get', `SELECT "startTime", "endTime", "locked", "category", "videoDuration" FROM "sponsorTimes" WHERE "videoID" = ?`, ["dQw4w9WgXZH"]);
+ if (row.startTime === 1 && row.endTime === 10 && row.locked === 0 && row.category === "sponsor" && row.videoDuration === 5010.20) {
+ done();
+ } else {
+ done("Submitted times were not saved. Actual submission: " + JSON.stringify(row));
+ }
+ } else {
+ done("Status code was " + res.status);
+ }
+ })
+ .catch(err => done(err));
+ });
+
+ it('Should be able to submit a single time with a duration in the body (JSON method)', (done: Done) => {
fetch(getbaseURL()
+ "/api/postVideoSponsorTimes", {
method: 'POST',
From c17f0b1e6e21a648a1d508810ec1235535065e7b Mon Sep 17 00:00:00 2001
From: Ajay Ramachandran
Date: Mon, 29 Mar 2021 18:39:55 -0400
Subject: [PATCH 19/32] Add invite link
---
src/routes/postSkipSegments.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/routes/postSkipSegments.ts b/src/routes/postSkipSegments.ts
index 7738653..0ab6de8 100644
--- a/src/routes/postSkipSegments.ts
+++ b/src/routes/postSkipSegments.ts
@@ -388,7 +388,7 @@ export async function postSkipSegments(req: Request, res: Response) {
+ segments[i].category + "'. A moderator has decided that no new segments are needed and that all current segments of this category are timed perfectly.\n\n "
+ (segments[i].category === "sponsor" ? "Maybe the segment you are submitting is a different category that you have not enabled and is not a sponsor. " +
"Categories that aren't sponsor, such as self-promotion can be enabled in the options.\n\n " : "")
- + "If you believe this is incorrect, please contact someone on Discord.",
+ + "If you believe this is incorrect, please contact someone on discord.gg/SponsorBlock or matrix.to/#/+sponsorblock:ajay.app",
);
return;
}
From c9a8dc21b15226e32d521a8dbde0b0517e90647a Mon Sep 17 00:00:00 2001
From: Ajay Ramachandran
Date: Mon, 29 Mar 2021 19:16:18 -0400
Subject: [PATCH 20/32] Unlock videos and hide segments if duration changed
---
databases/_upgrade_sponsorTimes_10.sql | 30 ++++++++++++++++++
src/app.ts | 4 +--
src/routes/deleteNoSegments.ts | 29 +++++++++++------
src/routes/getSkipSegments.ts | 4 +--
src/routes/postSkipSegments.ts | 40 ++++++++++++++++-------
test/cases/getSkipSegments.ts | 41 +++++++++++++++++-------
test/cases/getSkipSegmentsByHash.ts | 26 +++++++++++----
test/cases/postSkipSegments.ts | 44 ++++++++++++++++++++++++++
8 files changed, 175 insertions(+), 43 deletions(-)
create mode 100644 databases/_upgrade_sponsorTimes_10.sql
diff --git a/databases/_upgrade_sponsorTimes_10.sql b/databases/_upgrade_sponsorTimes_10.sql
new file mode 100644
index 0000000..174ceaf
--- /dev/null
+++ b/databases/_upgrade_sponsorTimes_10.sql
@@ -0,0 +1,30 @@
+BEGIN TRANSACTION;
+
+/* Add Hidden field */
+CREATE TABLE "sqlb_temp_table_10" (
+ "videoID" TEXT NOT NULL,
+ "startTime" REAL NOT NULL,
+ "endTime" REAL NOT NULL,
+ "votes" INTEGER NOT NULL,
+ "locked" INTEGER NOT NULL default '0',
+ "incorrectVotes" INTEGER NOT NULL default '1',
+ "UUID" TEXT NOT NULL UNIQUE,
+ "userID" TEXT NOT NULL,
+ "timeSubmitted" INTEGER NOT NULL,
+ "views" INTEGER NOT NULL,
+ "category" TEXT NOT NULL DEFAULT 'sponsor',
+ "service" TEXT NOT NULL DEFAULT 'YouTube',
+ "videoDuration" REAL NOT NULL DEFAULT '0',
+ "hidden" INTEGER NOT NULL DEFAULT '0',
+ "shadowHidden" INTEGER NOT NULL,
+ "hashedVideoID" TEXT NOT NULL default ''
+);
+
+INSERT INTO sqlb_temp_table_10 SELECT "videoID","startTime","endTime","votes","locked","incorrectVotes","UUID","userID","timeSubmitted","views","category","service","videoDuration",0,"shadowHidden","hashedVideoID" FROM "sponsorTimes";
+
+DROP TABLE "sponsorTimes";
+ALTER TABLE sqlb_temp_table_10 RENAME TO "sponsorTimes";
+
+UPDATE "config" SET value = 10 WHERE key = 'version';
+
+COMMIT;
\ No newline at end of file
diff --git a/src/app.ts b/src/app.ts
index 5bc2b2c..2af8835 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -5,7 +5,7 @@ import {oldGetVideoSponsorTimes} from './routes/oldGetVideoSponsorTimes';
import {postSegmentShift} from './routes/postSegmentShift';
import {postWarning} from './routes/postWarning';
import {getIsUserVIP} from './routes/getIsUserVIP';
-import {deleteNoSegments} from './routes/deleteNoSegments';
+import {deleteNoSegmentsEndpoint} from './routes/deleteNoSegments';
import {postNoSegments} from './routes/postNoSegments';
import {getUserInfo} from './routes/getUserInfo';
import {getDaysSavedFormatted} from './routes/getDaysSavedFormatted';
@@ -117,7 +117,7 @@ function setupRoutes(app: Express) {
//submit video containing no segments
app.post('/api/noSegments', postNoSegments);
- app.delete('/api/noSegments', deleteNoSegments);
+ app.delete('/api/noSegments', deleteNoSegmentsEndpoint);
//get if user is a vip
app.get('/api/isUserVIP', getIsUserVIP);
diff --git a/src/routes/deleteNoSegments.ts b/src/routes/deleteNoSegments.ts
index cfa6c78..5584c5a 100644
--- a/src/routes/deleteNoSegments.ts
+++ b/src/routes/deleteNoSegments.ts
@@ -2,12 +2,14 @@ import {Request, Response} from 'express';
import {isUserVIP} from '../utils/isUserVIP';
import {getHash} from '../utils/getHash';
import {db} from '../databases/databases';
+import { Category, VideoID } from '../types/segments.model';
+import { UserID } from '../types/user.model';
-export async function deleteNoSegments(req: Request, res: Response) {
+export async function deleteNoSegmentsEndpoint(req: Request, res: Response) {
// Collect user input data
- const videoID = req.body.videoID;
- let userID = req.body.userID;
- const categories = req.body.categories;
+ const videoID = req.body.videoID as VideoID;
+ const userID = req.body.userID as UserID;
+ const categories = req.body.categories as Category[];
// Check input data is valid
if (!videoID
@@ -23,8 +25,8 @@ export async function deleteNoSegments(req: Request, res: Response) {
}
// Check if user is VIP
- userID = getHash(userID);
- const userIsVIP = await isUserVIP(userID);
+ const hashedUserID = getHash(userID);
+ const userIsVIP = await isUserVIP(hashedUserID);
if (!userIsVIP) {
res.status(403).json({
@@ -33,13 +35,22 @@ export async function deleteNoSegments(req: Request, res: Response) {
return;
}
+ deleteNoSegments(videoID, categories);
+
+ res.status(200).json({message: 'Removed no segments entrys for video ' + videoID});
+}
+
+/**
+ *
+ * @param videoID
+ * @param categories If null, will remove all
+ */
+export async function deleteNoSegments(videoID: VideoID, categories: Category[]): Promise {
const entries = (await db.prepare("all", 'SELECT * FROM "noSegments" WHERE "videoID" = ?', [videoID])).filter((entry: any) => {
- return (categories.indexOf(entry.category) !== -1);
+ return categories === null || categories.indexOf(entry.category) !== -1;
});
for (const entry of entries) {
await db.prepare('run', 'DELETE FROM "noSegments" WHERE "videoID" = ? AND "category" = ?', [videoID, entry.category]);
}
-
- res.status(200).json({message: 'Removed no segments entrys for video ' + videoID});
}
diff --git a/src/routes/getSkipSegments.ts b/src/routes/getSkipSegments.ts
index c2a9155..d36a91a 100644
--- a/src/routes/getSkipSegments.ts
+++ b/src/routes/getSkipSegments.ts
@@ -60,7 +60,7 @@ async function getSegmentsByVideoID(req: Request, videoID: string, categories: C
.prepare(
'all',
`SELECT "startTime", "endTime", "votes", "locked", "UUID", "category", "videoDuration", "shadowHidden" FROM "sponsorTimes"
- WHERE "videoID" = ? AND "category" IN (${categories.map((c) => "'" + c + "'")}) AND "service" = ? ORDER BY "startTime"`,
+ WHERE "videoID" = ? AND "category" IN (${categories.map((c) => "'" + c + "'")}) AND "service" = ? AND "hidden" = 0 ORDER BY "startTime"`,
[videoID, service]
)).reduce((acc: SBRecord, segment: DBSegment) => {
acc[segment.category] = acc[segment.category] || [];
@@ -132,7 +132,7 @@ async function getSegmentsFromDB(hashedVideoIDPrefix: VideoIDHash, service: Serv
.prepare(
'all',
`SELECT "videoID", "startTime", "endTime", "votes", "locked", "UUID", "category", "videoDuration", "shadowHidden", "hashedVideoID" FROM "sponsorTimes"
- WHERE "hashedVideoID" LIKE ? AND "service" = ? ORDER BY "startTime"`,
+ WHERE "hashedVideoID" LIKE ? AND "service" = ? AND "hidden" = 0 ORDER BY "startTime"`,
[hashedVideoIDPrefix + '%', service]
);
diff --git a/src/routes/postSkipSegments.ts b/src/routes/postSkipSegments.ts
index 0ab6de8..37c4985 100644
--- a/src/routes/postSkipSegments.ts
+++ b/src/routes/postSkipSegments.ts
@@ -13,7 +13,8 @@ import {dispatchEvent} from '../utils/webhookUtils';
import {Request, Response} from 'express';
import { skipSegmentsHashKey, skipSegmentsKey } from '../middleware/redisKeys';
import redis from '../utils/redis';
-import { Category, IncomingSegment, Segment, Service, VideoDuration, VideoID } from '../types/segments.model';
+import { Category, IncomingSegment, Segment, SegmentUUID, Service, VideoDuration, VideoID } from '../types/segments.model';
+import { deleteNoSegments } from './deleteNoSegments';
interface APIVideoInfo {
err: string | boolean,
@@ -357,7 +358,7 @@ export async function postSkipSegments(req: Request, res: Response) {
return res.status(403).send('Submission rejected due to a warning from a moderator. This means that we noticed you were making some common mistakes that are not malicious, and we just want to clarify the rules. Could you please send a message in Discord or Matrix so we can further help you?');
}
- const noSegmentList = (await db.prepare('all', 'SELECT category from "noSegments" where "videoID" = ?', [videoID])).map((list: any) => {
+ let noSegmentList = (await db.prepare('all', 'SELECT category from "noSegments" where "videoID" = ?', [videoID])).map((list: any) => {
return list.category;
});
@@ -366,6 +367,31 @@ export async function postSkipSegments(req: Request, res: Response) {
const decreaseVotes = 0;
+ let apiVideoInfo: APIVideoInfo = null;
+ if (service == Service.YouTube) {
+ apiVideoInfo = await getYouTubeVideoInfo(videoID);
+ }
+ const apiVideoDuration = getYouTubeVideoDuration(apiVideoInfo);
+ if (!videoDuration || (apiVideoDuration && Math.abs(videoDuration - apiVideoDuration) > 2)) {
+ // If api duration is far off, take that one instead (it is only precise to seconds, not millis)
+ videoDuration = apiVideoDuration || 0 as VideoDuration;
+ }
+
+ const previousSubmissions = await db.prepare('all', `SELECT "videoDuration", "UUID" FROM "sponsorTimes" WHERE "videoID" = ? AND "service" = ? AND "hidden" = 0 AND "shadowHidden" = 0 AND "votes" >= 0`, [videoID, service]) as
+ {videoDuration: VideoDuration, UUID: SegmentUUID}[];
+ // If the video's duration is changed, then the video should be unlocked and old submissions should be hidden
+ const videoDurationChanged = previousSubmissions.length > 0 && !previousSubmissions.some((e) => Math.abs(videoDuration - e.videoDuration) < 2);
+ if (videoDurationChanged) {
+ // Hide all previous submissions
+ for (const submission of previousSubmissions) {
+ await db.prepare('run', `UPDATE "sponsorTimes" SET "hidden" = 1 WHERE "UUID" = ?`, [submission.UUID]);
+ }
+
+ // Reset no segments
+ noSegmentList = [];
+ deleteNoSegments(videoID, null);
+ }
+
// Check if all submissions are correct
for (let i = 0; i < segments.length; i++) {
if (segments[i] === undefined || segments[i].segment === undefined || segments[i].category === undefined) {
@@ -419,16 +445,6 @@ export async function postSkipSegments(req: Request, res: Response) {
}
}
- let apiVideoInfo: APIVideoInfo = null;
- if (service == Service.YouTube) {
- apiVideoInfo = await getYouTubeVideoInfo(videoID);
- }
- const apiVideoDuration = getYouTubeVideoDuration(apiVideoInfo);
- if (!videoDuration || (apiVideoDuration && Math.abs(videoDuration - apiVideoDuration) > 2)) {
- // If api duration is far off, take that one instead (it is only precise to seconds, not millis)
- videoDuration = apiVideoDuration || 0 as VideoDuration;
- }
-
// Auto moderator check
if (!isVIP && service == Service.YouTube) {
const autoModerateResult = await autoModerateSubmission(apiVideoInfo, {userID, videoID, segments});//startTime, endTime, category: segments[i].category});
diff --git a/test/cases/getSkipSegments.ts b/test/cases/getSkipSegments.ts
index 674a773..f40203c 100644
--- a/test/cases/getSkipSegments.ts
+++ b/test/cases/getSkipSegments.ts
@@ -5,18 +5,19 @@ import {getHash} from '../../src/utils/getHash';
describe('getSkipSegments', () => {
before(async () => {
- let startOfQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "timeSubmitted", views, category, "service", "videoDuration", "shadowHidden", "hashedVideoID") VALUES';
- await db.prepare("run", startOfQuery + "('testtesttest', 1, 11, 2, 0, '1-uuid-0', 'testman', 0, 50, 'sponsor', 'YouTube', 100, 0, '" + getHash('testtesttest', 1) + "')");
- await db.prepare("run", startOfQuery + "('testtesttest2', 1, 11, 2, 0, '1-uuid-0-1', 'testman', 0, 50, 'sponsor', 'PeerTube', 120, 0, '" + getHash('testtesttest2', 1) + "')");
- await db.prepare("run", startOfQuery + "('testtesttest', 20, 33, 2, 0, '1-uuid-2', 'testman', 0, 50, 'intro', 'YouTube', 101, 0, '" + getHash('testtesttest', 1) + "')");
- await db.prepare("run", startOfQuery + "('testtesttest,test', 1, 11, 2, 0, '1-uuid-1', 'testman', 0, 50, 'sponsor', 'YouTube', 140, 0, '" + getHash('testtesttest,test', 1) + "')");
- await db.prepare("run", startOfQuery + "('test3', 1, 11, 2, 0, '1-uuid-4', 'testman', 0, 50, 'sponsor', 'YouTube', 200, 0, '" + getHash('test3', 1) + "')");
- await db.prepare("run", startOfQuery + "('test3', 7, 22, -3, 0, '1-uuid-5', 'testman', 0, 50, 'sponsor', 'YouTube', 300, 0, '" + getHash('test3', 1) + "')");
- await db.prepare("run", startOfQuery + "('multiple', 1, 11, 2, 0, '1-uuid-6', 'testman', 0, 50, 'intro', 'YouTube', 400, 0, '" + getHash('multiple', 1) + "')");
- await db.prepare("run", startOfQuery + "('multiple', 20, 33, 2, 0, '1-uuid-7', 'testman', 0, 50, 'intro', 'YouTube', 500, 0, '" + getHash('multiple', 1) + "')");
- await db.prepare("run", startOfQuery + "('locked', 20, 33, 2, 1, '1-uuid-locked-8', 'testman', 0, 50, 'intro', 'YouTube', 230, 0, '" + getHash('locked', 1) + "')");
- await db.prepare("run", startOfQuery + "('locked', 20, 34, 100000, 0, '1-uuid-9', 'testman', 0, 50, 'intro', 'YouTube', 190, 0, '" + getHash('locked', 1) + "')");
-
+ let startOfQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "timeSubmitted", views, category, "service", "videoDuration", "hidden", "shadowHidden", "hashedVideoID") VALUES';
+ await db.prepare("run", startOfQuery + "('testtesttest', 1, 11, 2, 0, '1-uuid-0', 'testman', 0, 50, 'sponsor', 'YouTube', 100, 0, 0, '" + getHash('testtesttest', 1) + "')");
+ await db.prepare("run", startOfQuery + "('testtesttest2', 1, 11, 2, 0, '1-uuid-0-1', 'testman', 0, 50, 'sponsor', 'PeerTube', 120, 0, 0, '" + getHash('testtesttest2', 1) + "')");
+ await db.prepare("run", startOfQuery + "('testtesttest', 20, 33, 2, 0, '1-uuid-2', 'testman', 0, 50, 'intro', 'YouTube', 101, 0, 0, '" + getHash('testtesttest', 1) + "')");
+ await db.prepare("run", startOfQuery + "('testtesttest,test', 1, 11, 2, 0, '1-uuid-1', 'testman', 0, 50, 'sponsor', 'YouTube', 140, 0, 0, '" + getHash('testtesttest,test', 1) + "')");
+ await db.prepare("run", startOfQuery + "('test3', 1, 11, 2, 0, '1-uuid-4', 'testman', 0, 50, 'sponsor', 'YouTube', 200, 0, 0, '" + getHash('test3', 1) + "')");
+ await db.prepare("run", startOfQuery + "('test3', 7, 22, -3, 0, '1-uuid-5', 'testman', 0, 50, 'sponsor', 'YouTube', 300, 0, 0, '" + getHash('test3', 1) + "')");
+ await db.prepare("run", startOfQuery + "('multiple', 1, 11, 2, 0, '1-uuid-6', 'testman', 0, 50, 'intro', 'YouTube', 400, 0, 0, '" + getHash('multiple', 1) + "')");
+ await db.prepare("run", startOfQuery + "('multiple', 20, 33, 2, 0, '1-uuid-7', 'testman', 0, 50, 'intro', 'YouTube', 500, 0, 0, '" + getHash('multiple', 1) + "')");
+ await db.prepare("run", startOfQuery + "('locked', 20, 33, 2, 1, '1-uuid-locked-8', 'testman', 0, 50, 'intro', 'YouTube', 230, 0, 0, '" + getHash('locked', 1) + "')");
+ await db.prepare("run", startOfQuery + "('locked', 20, 34, 100000, 0, '1-uuid-9', 'testman', 0, 50, 'intro', 'YouTube', 190, 0, 0, '" + getHash('locked', 1) + "')");
+ await db.prepare("run", startOfQuery + "('onlyHiddenSegments', 20, 34, 100000, 0, 'onlyHiddenSegments', 'testman', 0, 50, 'sponsor', 'YouTube', 190, 1, 0, '" + getHash('onlyHiddenSegments', 1) + "')");
+
return;
});
@@ -106,6 +107,22 @@ describe('getSkipSegments', () => {
.catch(err => ("Couldn't call endpoint"));
});
+ it('Should be empty if all submissions are hidden', () => {
+ fetch(getbaseURL() + "/api/skipSegments?videoID=onlyHiddenSegments")
+ .then(async res => {
+ if (res.status !== 200) return ("Status code was: " + res.status);
+ else {
+ const data = await res.json();
+ if (data.length === 0) {
+ return;
+ } else {
+ return ("Received incorrect body: " + (await res.text()));
+ }
+ }
+ })
+ .catch(err => ("Couldn't call endpoint"));
+ });
+
it('Should be able to get multiple times by category', () => {
fetch(getbaseURL() + "/api/skipSegments?videoID=multiple&categories=[\"intro\"]")
.then(async res => {
diff --git a/test/cases/getSkipSegmentsByHash.ts b/test/cases/getSkipSegmentsByHash.ts
index fd45884..23d9d49 100644
--- a/test/cases/getSkipSegmentsByHash.ts
+++ b/test/cases/getSkipSegmentsByHash.ts
@@ -12,12 +12,13 @@ sinonStub.callsFake(YouTubeApiMock.listVideos);
describe('getSegmentsByHash', () => {
before(async () => {
- let startOfQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "UUID", "userID", "timeSubmitted", views, category, "service", "shadowHidden", "hashedVideoID") VALUES';
- await db.prepare("run", startOfQuery + "('getSegmentsByHash-0', 1, 10, 2, 'getSegmentsByHash-0-0', 'testman', 0, 50, 'sponsor', 'YouTube', 0, '" + getHash('getSegmentsByHash-0', 1) + "')"); // hash = fdaff4dee1043451faa7398324fb63d8618ebcd11bddfe0491c488db12c6c910
- await db.prepare("run", startOfQuery + "('getSegmentsByHash-0', 1, 10, 2, 'getSegmentsByHash-0-0-1', 'testman', 0, 50, 'sponsor', 'PeerTube', 0, '" + getHash('getSegmentsByHash-0', 1) + "')"); // hash = fdaff4dee1043451faa7398324fb63d8618ebcd11bddfe0491c488db12c6c910
- await db.prepare("run", startOfQuery + "('getSegmentsByHash-0', 20, 30, 2, 'getSegmentsByHash-0-1', 'testman', 100, 150, 'intro', 'YouTube', 0, '" + getHash('getSegmentsByHash-0', 1) + "')"); // hash = fdaff4dee1043451faa7398324fb63d8618ebcd11bddfe0491c488db12c6c910
- await db.prepare("run", startOfQuery + "('getSegmentsByHash-noMatchHash', 40, 50, 2, 'getSegmentsByHash-noMatchHash', 'testman', 0, 50, 'sponsor', 'YouTube', 0, 'fdaffnoMatchHash')"); // hash = fdaff4dee1043451faa7398324fb63d8618ebcd11bddfe0491c488db12c6c910
- await db.prepare("run", startOfQuery + "('getSegmentsByHash-1', 60, 70, 2, 'getSegmentsByHash-1', 'testman', 0, 50, 'sponsor', 'YouTube', 0, '" + getHash('getSegmentsByHash-1', 1) + "')"); // hash = 3272fa85ee0927f6073ef6f07ad5f3146047c1abba794cfa364d65ab9921692b
+ let startOfQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "UUID", "userID", "timeSubmitted", views, category, "service", "hidden", "shadowHidden", "hashedVideoID") VALUES';
+ await db.prepare("run", startOfQuery + "('getSegmentsByHash-0', 1, 10, 2, 'getSegmentsByHash-0-0', 'testman', 0, 50, 'sponsor', 'YouTube', 0, 0, '" + getHash('getSegmentsByHash-0', 1) + "')"); // hash = fdaff4dee1043451faa7398324fb63d8618ebcd11bddfe0491c488db12c6c910
+ await db.prepare("run", startOfQuery + "('getSegmentsByHash-0', 1, 10, 2, 'getSegmentsByHash-0-0-1', 'testman', 0, 50, 'sponsor', 'PeerTube', 0, 0, '" + getHash('getSegmentsByHash-0', 1) + "')"); // hash = fdaff4dee1043451faa7398324fb63d8618ebcd11bddfe0491c488db12c6c910
+ await db.prepare("run", startOfQuery + "('getSegmentsByHash-0', 20, 30, 2, 'getSegmentsByHash-0-1', 'testman', 100, 150, 'intro', 'YouTube', 0, 0, '" + getHash('getSegmentsByHash-0', 1) + "')"); // hash = fdaff4dee1043451faa7398324fb63d8618ebcd11bddfe0491c488db12c6c910
+ await db.prepare("run", startOfQuery + "('getSegmentsByHash-noMatchHash', 40, 50, 2, 'getSegmentsByHash-noMatchHash', 'testman', 0, 50, 'sponsor', 'YouTube', 0, 0, 'fdaffnoMatchHash')"); // hash = fdaff4dee1043451faa7398324fb63d8618ebcd11bddfe0491c488db12c6c910
+ await db.prepare("run", startOfQuery + "('getSegmentsByHash-1', 60, 70, 2, 'getSegmentsByHash-1', 'testman', 0, 50, 'sponsor', 'YouTube', 0, 0, '" + getHash('getSegmentsByHash-1', 1) + "')"); // hash = 3272fa85ee0927f6073ef6f07ad5f3146047c1abba794cfa364d65ab9921692b
+ await db.prepare("run", startOfQuery + "('onlyHidden', 60, 70, 2, 'onlyHidden', 'testman', 0, 50, 'sponsor', 'YouTube', 1, 0, '" + getHash('onlyHidden', 1) + "')"); // hash = f3a199e1af001d716cdc6599360e2b062c2d2b3fa2885f6d9d2fd741166cbbd3
});
it('Should be able to get a 200', (done: Done) => {
@@ -55,6 +56,19 @@ describe('getSegmentsByHash', () => {
.catch(err => done("Couldn't call endpoint"));
});
+ it('Should be able to get an empty array if only hidden videos', (done: Done) => {
+ fetch(getbaseURL() + '/api/skipSegments/f3a1?categories=["sponsor"]')
+ .then(async res => {
+ if (res.status !== 404) done("non 404 status code, was " + res.status);
+ else {
+ const body = await res.text();
+ if (JSON.parse(body).length === 0 && body === '[]') done(); // pass
+ else done("non empty array returned");
+ }
+ })
+ .catch(err => done("Couldn't call endpoint"));
+ });
+
it('Should return 400 prefix too short', (done: Done) => {
fetch(getbaseURL() + '/api/skipSegments/11?categories=["shilling"]')
.then(res => {
diff --git a/test/cases/postSkipSegments.ts b/test/cases/postSkipSegments.ts
index 399e922..512c2ee 100644
--- a/test/cases/postSkipSegments.ts
+++ b/test/cases/postSkipSegments.ts
@@ -6,6 +6,7 @@ import {db} from '../../src/databases/databases';
import {ImportMock} from 'ts-mock-imports';
import * as YouTubeAPIModule from '../../src/utils/youtubeApi';
import {YouTubeApiMock} from '../youtubeMock';
+import e from 'express';
const mockManager = ImportMock.mockStaticClass(YouTubeAPIModule, 'YouTubeAPI');
const sinonStub = mockManager.mock('listVideos');
@@ -193,6 +194,49 @@ describe('postSkipSegments', () => {
.catch(err => done(err));
});
+ it('Should be able to submit with a new duration, and hide old submissions and remove segment locks', async () => {
+ await db.prepare("run", `INSERT INTO "noSegments" ("userID", "videoID", "category")
+ VALUES ('` + getHash("VIPUser-noSegments") + "', 'noDuration', 'sponsor')");
+
+ try {
+ const res = await fetch(getbaseURL()
+ + "/api/postVideoSponsorTimes", {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ userID: "test",
+ videoID: "noDuration",
+ videoDuration: 100,
+ segments: [{
+ segment: [1, 10],
+ category: "sponsor",
+ }],
+ }),
+ });
+
+ if (res.status === 200) {
+ const noSegmentsRow = await db.prepare('get', `SELECT * from "noSegments" WHERE videoID = ?`, ["noDuration"]);
+ const videoRows = await db.prepare('all', `SELECT "startTime", "endTime", "locked", "category", "videoDuration"
+ FROM "sponsorTimes" WHERE "videoID" = ? AND hidden = 0`, ["noDuration"]);
+ const videoRow = videoRows[0];
+ const hiddenVideoRows = await db.prepare('all', `SELECT "startTime", "endTime", "locked", "category", "videoDuration"
+ FROM "sponsorTimes" WHERE "videoID" = ? AND hidden = 1`, ["noDuration"]);
+ if (noSegmentsRow === undefined && videoRows.length === 1 && hiddenVideoRows.length === 1 && videoRow.startTime === 1 && videoRow.endTime === 10
+ && videoRow.locked === 0 && videoRow.category === "sponsor" && videoRow.videoDuration === 100) {
+ return;
+ } else {
+ return "Submitted times were not saved. Actual submission: " + JSON.stringify(videoRow);
+ }
+ } else {
+ return "Status code was " + res.status;
+ }
+ } catch (e) {
+ return e;
+ }
+ });
+
it('Should be able to submit a single time under a different service (JSON method)', (done: Done) => {
fetch(getbaseURL()
+ "/api/postVideoSponsorTimes", {
From 93d724202196917d48770e7962dd80252928bd64 Mon Sep 17 00:00:00 2001
From: Ajay Ramachandran
Date: Tue, 30 Mar 2021 22:50:09 -0400
Subject: [PATCH 21/32] Add preview category
---
src/config.ts | 2 +-
test.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/config.ts b/src/config.ts
index 93c6a43..7b15987 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -16,7 +16,7 @@ addDefaults(config, {
privateDBSchema: "./databases/_private.db.sql",
readOnly: false,
webhooks: [],
- categoryList: ["sponsor", "intro", "outro", "interaction", "selfpromo", "music_offtopic"],
+ categoryList: ["sponsor", "selfpromo", "interaction", "intro", "outro", "preview", "music_offtopic"],
maxNumberOfActiveWarnings: 3,
hoursAfterWarningExpires: 24,
adminUserID: "",
diff --git a/test.json b/test.json
index add6ebf..58eb72a 100644
--- a/test.json
+++ b/test.json
@@ -49,7 +49,7 @@
]
}
],
- "categoryList": ["sponsor", "intro", "outro", "interaction", "selfpromo", "music_offtopic"],
+ "categoryList": ["sponsor", "selfpromo", "interaction", "intro", "outro", "preview", "music_offtopic"],
"maxNumberOfActiveWarnings": 3,
"hoursAfterWarningExpires": 24,
"rateLimit": {
From 1eca55d96c4a327c3dbd89af6d4cc74ad24afc22 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 31 Mar 2021 21:52:03 +0000
Subject: [PATCH 22/32] Bump y18n from 4.0.0 to 4.0.1
Bumps [y18n](https://github.com/yargs/y18n) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/yargs/y18n/releases)
- [Changelog](https://github.com/yargs/y18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yargs/y18n/commits)
Signed-off-by: dependabot[bot]
---
package-lock.json | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index dd69f2e..f17b109 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2989,9 +2989,9 @@
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
},
"y18n": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
- "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz",
+ "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==",
"dev": true
},
"yallist": {
From cfbf8a47d7664ea212392bf016c8cec1612fa295 Mon Sep 17 00:00:00 2001
From: Ajay Ramachandran
Date: Fri, 2 Apr 2021 12:03:21 -0400
Subject: [PATCH 23/32] Fix hashing function
---
src/databases/Postgres.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/databases/Postgres.ts b/src/databases/Postgres.ts
index e7e16f4..779ebf2 100644
--- a/src/databases/Postgres.ts
+++ b/src/databases/Postgres.ts
@@ -136,7 +136,7 @@ export class Postgres implements IDatabase {
private processUpgradeQuery(query: string): string {
let result = query;
- result = result.replace(/sha256\((.*?)\)/gm, "digest($1, 'sha256')");
+ result = result.replace(/sha256\((.*?)\)/gm, "encode(digest($1, 'sha256'), 'hex')");
result = result.replace(/integer/gmi, "NUMERIC");
return result;
From bc688a3d8dcc765a12618559ff20819bbd7d58a8 Mon Sep 17 00:00:00 2001
From: Ajay Ramachandran
Date: Fri, 2 Apr 2021 16:40:51 -0400
Subject: [PATCH 24/32] FIx duration issue
---
src/routes/postSkipSegments.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/routes/postSkipSegments.ts b/src/routes/postSkipSegments.ts
index 37c4985..1f380da 100644
--- a/src/routes/postSkipSegments.ts
+++ b/src/routes/postSkipSegments.ts
@@ -377,7 +377,8 @@ export async function postSkipSegments(req: Request, res: Response) {
videoDuration = apiVideoDuration || 0 as VideoDuration;
}
- const previousSubmissions = await db.prepare('all', `SELECT "videoDuration", "UUID" FROM "sponsorTimes" WHERE "videoID" = ? AND "service" = ? AND "hidden" = 0 AND "shadowHidden" = 0 AND "votes" >= 0`, [videoID, service]) as
+ const previousSubmissions = await db.prepare('all', `SELECT "videoDuration", "UUID" FROM "sponsorTimes" WHERE "videoID" = ? AND "service" = ? AND "hidden" = 0
+ AND "shadowHidden" = 0 AND "votes" >= 0 AND "videoDuration" != 0`, [videoID, service]) as
{videoDuration: VideoDuration, UUID: SegmentUUID}[];
// If the video's duration is changed, then the video should be unlocked and old submissions should be hidden
const videoDurationChanged = previousSubmissions.length > 0 && !previousSubmissions.some((e) => Math.abs(videoDuration - e.videoDuration) < 2);
From 77a4c2fe34d67002b2427d921c8599cd224ab03d Mon Sep 17 00:00:00 2001
From: Ajay Ramachandran
Date: Tue, 13 Apr 2021 03:04:02 +0200
Subject: [PATCH 25/32] Update nginx config
---
nginx/nginx.conf | 87 +++++++++++++++++++++++++++++++++++-------------
1 file changed, 63 insertions(+), 24 deletions(-)
diff --git a/nginx/nginx.conf b/nginx/nginx.conf
index daee81f..76a77d3 100644
--- a/nginx/nginx.conf
+++ b/nginx/nginx.conf
@@ -2,7 +2,7 @@ worker_processes 8;
worker_rlimit_nofile 8192;
events {
- worker_connections 32768; ## Default: 1024
+ worker_connections 132768; ## Default: 1024
}
http {
@@ -12,19 +12,35 @@ http {
upstream backend_GET {
least_conn;
- server localhost:4442;
- server localhost:4443;
- server localhost:4444;
- server localhost:4445;
- server localhost:4446;
+ server localhost:4441;
+ server localhost:4442;
+ #server localhost:4443;
+ #server localhost:4444;
+ #server localhost:4445;
+ #server localhost:4446;
#server localhost:4447;
#server localhost:4448;
+
+ server 10.0.0.3:4441;
+ server 10.0.0.3:4442;
+
+ #server 134.209.69.251:80 backup;
+
+ server 116.203.32.253:80 backup;
+ #server 116.203.32.253:80;
}
upstream backend_POST {
- server localhost:4441;
+ #server localhost:4441;
+ #server localhost:4442;
+ server 10.0.0.3:4441;
+ #server 10.0.0.3:4442;
+ }
+ upstream backend_db {
+ #server localhost:4441;
+ server 10.0.0.3:4441;
}
- proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=CACHEZONE:10m inactive=60m max_size=40m;
+ proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=CACHEZONE:10m inactive=60m max_size=400m;
proxy_cache_key "$scheme$request_method$host$request_uri";
add_header X-Cache $upstream_cache_status;
@@ -43,14 +59,16 @@ http {
# internal;
#}
+ #proxy_send_timeout 120s;
+
location @myerrordirective_500 {
- return 502 "Internal Server Error";
+ return 400 "Internal Server Error";
}
location @myerrordirective_502 {
- return 502 "Bad Gateway";
+ return 400 "Bad Gateway";
}
location @myerrordirective_504 {
- return 502 "Gateway Timeout";
+ return 400 "Gateway Timeout";
}
@@ -62,17 +80,16 @@ http {
return 301 https://sb.ltn.fi;
}
- location /invidious/ {
- proxy_pass https://invidious.fdn.fr/;
- }
-
location /test/ {
proxy_pass http://localhost:4440/;
#proxy_pass https://sbtest.etcinit.com/;
}
location /api/skipSegments {
- proxy_pass http://backend_$request_method;
+ #return 200 "[]";
+ proxy_pass http://backend_$request_method;
+ #proxy_cache CACHEZONE;
+ #proxy_cache_valid 2m;
}
location /api/getTopUsers {
@@ -83,24 +100,43 @@ http {
location /api/getTotalStats {
proxy_pass http://backend_GET;
- }
+ #return 200 "";
+ }
location /api/getVideoSponsorTimes {
proxy_pass http://backend_GET;
}
-
- location = /database.db {
- alias /home/sbadmin/sponsor/databases/sponsorTimes.db;
+
+ location /database {
+ proxy_pass http://backend_db;
}
-
+
+ location = /database.db {
+ #return 404 "Sqlite database has been replaced with csv exports at https://sponsor.ajay.app/database. Sqlite exports might come back soon, but exported at longer intervals.";
+ #alias /home/sbadmin/sponsor/databases/sponsorTimes.db;
+ alias /home/sbadmin/test-db/database.db;
+ }
+
+ location = /database/sponsorTimes.csv {
+ alias /home/sbadmin/sponsorTimes.csv;
+ }
+
+ #location /api/voteOnSponsorTime {
+ # return 200 "Success";
+ #}
+
+ #location /api/viewedVideoSponsorTime {
+ # return 200 "Success";
+ #}
+
location /api {
proxy_pass http://backend_POST;
}
location / {
- root /home/sbadmin/caddy/SponsorBlockSite/public-prod;
-
+ root /home/sbadmin/SponsorBlockSite/public-prod;
+
### CORS
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
@@ -132,12 +168,15 @@ http {
}
- listen 443 ssl; # managed by Certbot
+ listen 443 default_server ssl; # managed by Certbot
+ #listen 80;
ssl_certificate /etc/letsencrypt/live/sponsor.ajay.app/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/sponsor.ajay.app/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
+
+
}
From 5b2f05741ed8e35d114123fab4cc2717b0c97bb4 Mon Sep 17 00:00:00 2001
From: Ajay Ramachandran
Date: Sat, 17 Apr 2021 16:23:45 -0400
Subject: [PATCH 26/32] Fix no segments votes being allowed
---
src/routes/voteOnSponsorTime.ts | 76 +++++++++++++++++----------------
test/cases/voteOnSponsorTime.ts | 21 +++++----
2 files changed, 52 insertions(+), 45 deletions(-)
diff --git a/src/routes/voteOnSponsorTime.ts b/src/routes/voteOnSponsorTime.ts
index 29f0e66..a1e81e7 100644
--- a/src/routes/voteOnSponsorTime.ts
+++ b/src/routes/voteOnSponsorTime.ts
@@ -175,51 +175,54 @@ async function categoryVote(UUID: string, userID: string, isVIP: boolean, isOwnS
const timeSubmitted = Date.now();
const voteAmount = isVIP ? 500 : 1;
+ const ableToVote = isVIP || finalResponse.finalStatus === 200 || true;
- // Add the vote
- if ((await db.prepare('get', `select count(*) as count from "categoryVotes" where "UUID" = ? and category = ?`, [UUID, category])).count > 0) {
- // Update the already existing db entry
- await db.prepare('run', `update "categoryVotes" set "votes" = "votes" + ? where "UUID" = ? and "category" = ?`, [voteAmount, UUID, category]);
- } else {
- // Add a db entry
- await db.prepare('run', `insert into "categoryVotes" ("UUID", "category", "votes") values (?, ?, ?)`, [UUID, category, voteAmount]);
- }
+ if (ableToVote) {
+ // Add the vote
+ if ((await db.prepare('get', `select count(*) as count from "categoryVotes" where "UUID" = ? and category = ?`, [UUID, category])).count > 0) {
+ // Update the already existing db entry
+ await db.prepare('run', `update "categoryVotes" set "votes" = "votes" + ? where "UUID" = ? and "category" = ?`, [voteAmount, UUID, category]);
+ } else {
+ // Add a db entry
+ await db.prepare('run', `insert into "categoryVotes" ("UUID", "category", "votes") values (?, ?, ?)`, [UUID, category, voteAmount]);
+ }
- // Add the info into the private db
- if (usersLastVoteInfo?.votes > 0) {
- // Reverse the previous vote
- await db.prepare('run', `update "categoryVotes" set "votes" = "votes" - ? where "UUID" = ? and "category" = ?`, [voteAmount, UUID, usersLastVoteInfo.category]);
+ // Add the info into the private db
+ if (usersLastVoteInfo?.votes > 0) {
+ // Reverse the previous vote
+ await db.prepare('run', `update "categoryVotes" set "votes" = "votes" - ? where "UUID" = ? and "category" = ?`, [voteAmount, UUID, usersLastVoteInfo.category]);
- await privateDB.prepare('run', `update "categoryVotes" set "category" = ?, "timeSubmitted" = ?, "hashedIP" = ? where "userID" = ? and "UUID" = ?`, [category, timeSubmitted, hashedIP, userID, UUID]);
- } else {
- await privateDB.prepare('run', `insert into "categoryVotes" ("UUID", "userID", "hashedIP", "category", "timeSubmitted") values (?, ?, ?, ?, ?)`, [UUID, userID, hashedIP, category, timeSubmitted]);
- }
+ await privateDB.prepare('run', `update "categoryVotes" set "category" = ?, "timeSubmitted" = ?, "hashedIP" = ? where "userID" = ? and "UUID" = ?`, [category, timeSubmitted, hashedIP, userID, UUID]);
+ } else {
+ await privateDB.prepare('run', `insert into "categoryVotes" ("UUID", "userID", "hashedIP", "category", "timeSubmitted") values (?, ?, ?, ?, ?)`, [UUID, userID, hashedIP, category, timeSubmitted]);
+ }
- // See if the submissions category is ready to change
- const currentCategoryInfo = await db.prepare("get", `select votes from "categoryVotes" where "UUID" = ? and category = ?`, [UUID, currentCategory.category]);
+ // See if the submissions category is ready to change
+ const currentCategoryInfo = await db.prepare("get", `select votes from "categoryVotes" where "UUID" = ? and category = ?`, [UUID, currentCategory.category]);
- const submissionInfo = await db.prepare("get", `SELECT "userID", "timeSubmitted", "votes" FROM "sponsorTimes" WHERE "UUID" = ?`, [UUID]);
- const isSubmissionVIP = submissionInfo && await isUserVIP(submissionInfo.userID);
- const startingVotes = isSubmissionVIP ? 10000 : 1;
+ const submissionInfo = await db.prepare("get", `SELECT "userID", "timeSubmitted", "votes" FROM "sponsorTimes" WHERE "UUID" = ?`, [UUID]);
+ const isSubmissionVIP = submissionInfo && await isUserVIP(submissionInfo.userID);
+ const startingVotes = isSubmissionVIP ? 10000 : 1;
- // Change this value from 1 in the future to make it harder to change categories
- // Done this way without ORs incase the value is zero
- const currentCategoryCount = (currentCategoryInfo === undefined || currentCategoryInfo === null) ? startingVotes : currentCategoryInfo.votes;
+ // Change this value from 1 in the future to make it harder to change categories
+ // Done this way without ORs incase the value is zero
+ const currentCategoryCount = (currentCategoryInfo === undefined || currentCategoryInfo === null) ? startingVotes : currentCategoryInfo.votes;
- // Add submission as vote
- if (!currentCategoryInfo && submissionInfo) {
- await db.prepare("run", `insert into "categoryVotes" ("UUID", "category", "votes") values (?, ?, ?)`, [UUID, currentCategory.category, currentCategoryCount]);
+ // Add submission as vote
+ if (!currentCategoryInfo && submissionInfo) {
+ await db.prepare("run", `insert into "categoryVotes" ("UUID", "category", "votes") values (?, ?, ?)`, [UUID, currentCategory.category, currentCategoryCount]);
- await privateDB.prepare("run", `insert into "categoryVotes" ("UUID", "userID", "hashedIP", "category", "timeSubmitted") values (?, ?, ?, ?, ?)`, [UUID, submissionInfo.userID, "unknown", currentCategory.category, submissionInfo.timeSubmitted]);
- }
+ await privateDB.prepare("run", `insert into "categoryVotes" ("UUID", "userID", "hashedIP", "category", "timeSubmitted") values (?, ?, ?, ?, ?)`, [UUID, submissionInfo.userID, "unknown", currentCategory.category, submissionInfo.timeSubmitted]);
+ }
- const nextCategoryCount = (nextCategoryInfo?.votes || 0) + voteAmount;
+ const nextCategoryCount = (nextCategoryInfo?.votes || 0) + voteAmount;
- //TODO: In the future, raise this number from zero to make it harder to change categories
- // VIPs change it every time
- if (nextCategoryCount - currentCategoryCount >= Math.max(Math.ceil(submissionInfo?.votes / 2), 2) || isVIP || isOwnSubmission) {
- // Replace the category
- await db.prepare('run', `update "sponsorTimes" set "category" = ? where "UUID" = ?`, [category, UUID]);
+ //TODO: In the future, raise this number from zero to make it harder to change categories
+ // VIPs change it every time
+ if (nextCategoryCount - currentCategoryCount >= Math.max(Math.ceil(submissionInfo?.votes / 2), 2) || isVIP || isOwnSubmission) {
+ // Replace the category
+ await db.prepare('run', `update "sponsorTimes" set "category" = ? where "UUID" = ?`, [category, UUID]);
+ }
}
res.sendStatus(finalResponse.finalStatus);
@@ -371,7 +374,8 @@ export async function voteOnSponsorTime(req: Request, res: Response) {
const ableToVote = isVIP
|| ((await db.prepare("get", `SELECT "userID" FROM "sponsorTimes" WHERE "userID" = ?`, [nonAnonUserID])) !== undefined
&& (await privateDB.prepare("get", `SELECT "userID" FROM "shadowBannedUsers" WHERE "userID" = ?`, [nonAnonUserID])) === undefined
- && (await privateDB.prepare("get", `SELECT "UUID" FROM "votes" WHERE "UUID" = ? AND "hashedIP" = ? AND "userID" != ?`, [UUID, hashedIP, userID])) === undefined);
+ && (await privateDB.prepare("get", `SELECT "UUID" FROM "votes" WHERE "UUID" = ? AND "hashedIP" = ? AND "userID" != ?`, [UUID, hashedIP, userID])) === undefined)
+ && finalResponse.finalStatus === 200;
if (ableToVote) {
//update the votes table
diff --git a/test/cases/voteOnSponsorTime.ts b/test/cases/voteOnSponsorTime.ts
index 47a1f19..2aae65f 100644
--- a/test/cases/voteOnSponsorTime.ts
+++ b/test/cases/voteOnSponsorTime.ts
@@ -410,12 +410,13 @@ describe('voteOnSponsorTime', () => {
it('Non-VIP should not be able to downvote on a segment with no-segments category', (done: Done) => {
fetch(getbaseURL()
- + "/api/voteOnSponsorTime?userID=no-segments-voter&UUID=no-sponsor-segments-uuid-0&type=0")
+ + "/api/voteOnSponsorTime?userID=randomID&UUID=no-sponsor-segments-uuid-0&type=0")
.then(async res => {
- if (res.status === 403) {
+ let row = await db.prepare('get', `SELECT "votes" FROM "sponsorTimes" WHERE "UUID" = ?`, ["no-sponsor-segments-uuid-0"]);
+ if (res.status === 403 && row.votes === 2) {
done();
} else {
- done("Status code was " + res.status + " instead of 403");
+ done("Status code was " + res.status + " instead of 403, row was " + JSON.stringify(row));
}
})
.catch(err => done(err));
@@ -423,12 +424,13 @@ describe('voteOnSponsorTime', () => {
it('Non-VIP should be able to upvote on a segment with no-segments category', (done: Done) => {
fetch(getbaseURL()
- + "/api/voteOnSponsorTime?userID=no-segments-voter&UUID=no-sponsor-segments-uuid-0&type=1")
+ + "/api/voteOnSponsorTime?userID=randomID&UUID=no-sponsor-segments-uuid-0&type=1")
.then(async res => {
- if (res.status === 200) {
+ let row = await db.prepare('get', `SELECT "votes" FROM "sponsorTimes" WHERE "UUID" = ?`, ["no-sponsor-segments-uuid-0"]);
+ if (res.status === 200 && row.votes === 3) {
done();
} else {
- done("Status code was " + res.status + " instead of 200");
+ done("Status code was " + res.status + " instead of 403, row was " + JSON.stringify(row));
}
})
.catch(err => done(err));
@@ -436,12 +438,13 @@ describe('voteOnSponsorTime', () => {
it('Non-VIP should not be able to category vote on a segment with no-segments category', (done: Done) => {
fetch(getbaseURL()
- + "/api/voteOnSponsorTime?userID=no-segments-voter&UUID=no-sponsor-segments-uuid-0&category=outro")
+ + "/api/voteOnSponsorTime?userID=randomID&UUID=no-sponsor-segments-uuid-0&category=outro")
.then(async res => {
- if (res.status === 403) {
+ let row = await db.prepare('get', `SELECT "category" FROM "sponsorTimes" WHERE "UUID" = ?`, ["no-sponsor-segments-uuid-0"]);
+ if (res.status === 403 && row.category === "sponsor") {
done();
} else {
- done("Status code was " + res.status + " instead of 403");
+ done("Status code was " + res.status + " instead of 403, row was " + JSON.stringify(row));
}
})
.catch(err => done(err));
From 9d06bda4f8d93942e39ec6aa1c7eb2f6eeedfdcd Mon Sep 17 00:00:00 2001
From: Ajay Ramachandran
Date: Sat, 17 Apr 2021 16:37:39 -0400
Subject: [PATCH 27/32] Don't allow downvoting dead submissions
---
src/routes/voteOnSponsorTime.ts | 12 +++++++++---
test/cases/voteOnSponsorTime.ts | 19 +++++++++++++++++--
2 files changed, 26 insertions(+), 5 deletions(-)
diff --git a/src/routes/voteOnSponsorTime.ts b/src/routes/voteOnSponsorTime.ts
index 240baf9..4cb7690 100644
--- a/src/routes/voteOnSponsorTime.ts
+++ b/src/routes/voteOnSponsorTime.ts
@@ -286,13 +286,19 @@ export async function voteOnSponsorTime(req: Request, res: Response) {
return categoryVote(UUID, nonAnonUserID, isVIP, isOwnSubmission, category, hashedIP, finalResponse, res);
}
- if (type == 1 && !isVIP && !isOwnSubmission) {
+ if (type !== undefined && !isVIP && !isOwnSubmission) {
// Check if upvoting hidden segment
const voteInfo = await db.prepare('get', `SELECT votes FROM "sponsorTimes" WHERE "UUID" = ?`, [UUID]);
if (voteInfo && voteInfo.votes <= -2) {
- res.status(403).send("Not allowed to upvote segment with too many downvotes unless you are VIP.");
- return;
+ if (type == 1) {
+ res.status(403).send("Not allowed to upvote segment with too many downvotes unless you are VIP.");
+ return;
+ } else if (type == 0) {
+ // Already downvoted enough, ignore
+ res.status(200).send();
+ return;
+ }
}
}
diff --git a/test/cases/voteOnSponsorTime.ts b/test/cases/voteOnSponsorTime.ts
index 2aae65f..86c3233 100644
--- a/test/cases/voteOnSponsorTime.ts
+++ b/test/cases/voteOnSponsorTime.ts
@@ -368,10 +368,25 @@ describe('voteOnSponsorTime', () => {
fetch(getbaseURL()
+ "/api/voteOnSponsorTime?userID=randomID2&UUID=vote-uuid-5&type=1")
.then(async res => {
- if (res.status === 403) {
+ let row = await db.prepare('get', `SELECT "votes" FROM "sponsorTimes" WHERE "UUID" = ?`, ["vote-uuid-5"]);
+ if (res.status === 403 && row.votes === -3) {
done();
} else {
- done("Status code was " + res.status + " instead of 403");
+ done("Status code was " + res.status + ", row is " + JSON.stringify(row));
+ }
+ })
+ .catch(err => done(err));
+ });
+
+ it('Non-VIP should not be able to downvote "dead" submission', (done: Done) => {
+ fetch(getbaseURL()
+ + "/api/voteOnSponsorTime?userID=randomID2&UUID=vote-uuid-5&type=0")
+ .then(async res => {
+ let row = await db.prepare('get', `SELECT "votes" FROM "sponsorTimes" WHERE "UUID" = ?`, ["vote-uuid-5"]);
+ if (res.status === 200 && row.votes === -3) {
+ done();
+ } else {
+ done("Status code was " + res.status + ", row is " + JSON.stringify(row));
}
})
.catch(err => done(err));
From 058c05a1f72f77276c21c53f07c4adc8d43f0cf3 Mon Sep 17 00:00:00 2001
From: Ajay Ramachandran
Date: Sun, 18 Apr 2021 04:49:05 +0200
Subject: [PATCH 28/32] Fix permission issues
---
docker/docker-compose.yml | 8 ++++----
nginx/nginx.conf | 10 +++++++---
2 files changed, 11 insertions(+), 7 deletions(-)
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 4ee69ee..f7408d3 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -7,9 +7,9 @@ services:
- database.env
volumes:
- database-data:/var/lib/postgresql/data
- - ./database-export/:/opt/exports
+ - ./database-export/:/opt/exports # To make this work, run chmod 777 ./database-exports
ports:
- - 127.0.0.1:5432:5432
+ - 5432:5432
redis:
container_name: redis
image: redis
@@ -17,7 +17,7 @@ services:
volumes:
- ./redis/redis.conf:/usr/local/etc/redis/redis.conf
ports:
- - 127.0.0.1:32773:6379
+ - 32773:6379
volumes:
- database-data:
\ No newline at end of file
+ database-data:
diff --git a/nginx/nginx.conf b/nginx/nginx.conf
index 76a77d3..6302691 100644
--- a/nginx/nginx.conf
+++ b/nginx/nginx.conf
@@ -108,6 +108,9 @@ http {
proxy_pass http://backend_GET;
}
+ location /database/ {
+ alias /home/sbadmin/sponsor/docker/database-export/;
+ }
location /database {
proxy_pass http://backend_db;
}
@@ -118,9 +121,10 @@ http {
alias /home/sbadmin/test-db/database.db;
}
- location = /database/sponsorTimes.csv {
- alias /home/sbadmin/sponsorTimes.csv;
- }
+ #location = /database/sponsorTimes.csv {
+ # alias /home/sbadmin/sponsorTimes.csv;
+ #}
+
#location /api/voteOnSponsorTime {
# return 200 "Success";
From a06ab724adab93c082cb7a972c859768304b8c24 Mon Sep 17 00:00:00 2001
From: Ajay Ramachandran
Date: Sat, 17 Apr 2021 23:06:39 -0400
Subject: [PATCH 29/32] Fix file locations + formatting
---
src/config.ts | 2 +-
src/routes/dumpDatabase.ts | 28 +++++++++++++---------------
2 files changed, 14 insertions(+), 16 deletions(-)
diff --git a/src/config.ts b/src/config.ts
index 0e985dc..6139a6e 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -49,7 +49,7 @@ addDefaults(config, {
dumpDatabase: {
enabled: true,
minTimeBetweenMs: 60000,
- appExportPath: '/opt/exports',
+ appExportPath: './docker/database-export',
postgresExportPath: '/opt/exports',
tables: [{
name: "sponsorTimes",
diff --git a/src/routes/dumpDatabase.ts b/src/routes/dumpDatabase.ts
index b7f2726..8e33440 100644
--- a/src/routes/dumpDatabase.ts
+++ b/src/routes/dumpDatabase.ts
@@ -2,11 +2,10 @@ import {db} from '../databases/databases';
import {Logger} from '../utils/logger';
import {Request, Response} from 'express';
import { config } from '../config';
-const util = require('util');
-const fs = require('fs');
-const path = require('path');
+import util from 'util';
+import fs from 'fs';
+import path from 'path';
const unlink = util.promisify(fs.unlink);
-const fstat = util.promisify(fs.fstat);
const ONE_MINUTE = 1000 * 60;
@@ -16,7 +15,7 @@ const styleHeader = `