Use reputation when sorting segments

This commit is contained in:
Ajay Ramachandran
2021-05-23 17:54:51 -04:00
parent 194c657ba7
commit 5c2ab9087a
5 changed files with 59 additions and 13 deletions

View File

@@ -0,0 +1,31 @@
BEGIN TRANSACTION;
/* Add Service field */
CREATE TABLE "sqlb_temp_table_12" (
"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',
"reputation" REAL NOT NULL DEFAULT 0,
"shadowHidden" INTEGER NOT NULL,
"hashedVideoID" TEXT NOT NULL default ''
);
INSERT INTO sqlb_temp_table_12 SELECT "videoID","startTime","endTime","votes","locked","incorrectVotes","UUID","userID","timeSubmitted","views","category","service","videoDuration","hidden",0,"shadowHidden","hashedVideoID" FROM "sponsorTimes";
DROP TABLE "sponsorTimes";
ALTER TABLE sqlb_temp_table_12 RENAME TO "sponsorTimes";
UPDATE "config" SET value = 12 WHERE key = 'version';
COMMIT;

View File

@@ -10,7 +10,7 @@ interface ReputationDBResult {
oldUpvotedSubmissions: number oldUpvotedSubmissions: number
} }
export async function getReputation(userID: UserID) { export async function getReputation(userID: UserID): Promise<number> {
const pastDate = Date.now() - 1000 * 1000 * 60 * 60 * 24 * 45; // 45 days ago const pastDate = Date.now() - 1000 * 1000 * 60 * 60 * 24 * 45; // 45 days ago
const fetchFromDB = () => db.prepare("get", const fetchFromDB = () => db.prepare("get",
`SELECT COUNT(*) AS "totalSubmissions", `SELECT COUNT(*) AS "totalSubmissions",
@@ -32,7 +32,7 @@ export async function getReputation(userID: UserID) {
} }
if (result.oldUpvotedSubmissions < 3 || result.upvotedSum < 5) { if (result.oldUpvotedSubmissions < 3 || result.upvotedSum < 5) {
return 0 return 0;
} }
return convertRange(Math.min(result.upvotedSum, 50), 5, 50, 0, 15); return convertRange(Math.min(result.upvotedSum, 50), 5, 50, 0, 15);

View File

@@ -9,6 +9,7 @@ import { getHash } from '../utils/getHash';
import { getIP } from '../utils/getIP'; import { getIP } from '../utils/getIP';
import { Logger } from '../utils/logger'; import { Logger } from '../utils/logger';
import { QueryCacher } from '../middleware/queryCacher' import { QueryCacher } from '../middleware/queryCacher'
import { getReputation } from '../middleware/reputation';
async function prepareCategorySegments(req: Request, videoID: VideoID, category: Category, segments: DBSegment[], cache: SegmentCache = {shadowHiddenSegmentIPs: {}}): Promise<Segment[]> { async function prepareCategorySegments(req: Request, videoID: VideoID, category: Category, segments: DBSegment[], cache: SegmentCache = {shadowHiddenSegmentIPs: {}}): Promise<Segment[]> {
@@ -41,7 +42,7 @@ async function prepareCategorySegments(req: Request, videoID: VideoID, category:
const filteredSegments = segments.filter((_, index) => shouldFilter[index]); const filteredSegments = segments.filter((_, index) => shouldFilter[index]);
const maxSegments = getCategoryActionType(category) === CategoryActionType.Skippable ? 32 : 1 const maxSegments = getCategoryActionType(category) === CategoryActionType.Skippable ? 32 : 1
return chooseSegments(filteredSegments, maxSegments).map((chosenSegment) => ({ return (await chooseSegments(filteredSegments, maxSegments)).map((chosenSegment) => ({
category, category,
segment: [chosenSegment.startTime, chosenSegment.endTime], segment: [chosenSegment.startTime, chosenSegment.endTime],
UUID: chosenSegment.UUID, UUID: chosenSegment.UUID,
@@ -128,7 +129,7 @@ async function getSegmentsFromDBByHash(hashedVideoIDPrefix: VideoIDHash, service
const fetchFromDB = () => db const fetchFromDB = () => db
.prepare( .prepare(
'all', 'all',
`SELECT "videoID", "startTime", "endTime", "votes", "locked", "UUID", "category", "videoDuration", "shadowHidden", "hashedVideoID" FROM "sponsorTimes" `SELECT "videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "category", "videoDuration", "reputation", "shadowHidden", "hashedVideoID" FROM "sponsorTimes"
WHERE "hashedVideoID" LIKE ? AND "service" = ? AND "hidden" = 0 ORDER BY "startTime"`, WHERE "hashedVideoID" LIKE ? AND "service" = ? AND "hidden" = 0 ORDER BY "startTime"`,
[hashedVideoIDPrefix + '%', service] [hashedVideoIDPrefix + '%', service]
) as Promise<DBSegment[]>; ) as Promise<DBSegment[]>;
@@ -144,7 +145,7 @@ async function getSegmentsFromDBByVideoID(videoID: VideoID, service: Service): P
const fetchFromDB = () => db const fetchFromDB = () => db
.prepare( .prepare(
'all', 'all',
`SELECT "startTime", "endTime", "votes", "locked", "UUID", "category", "videoDuration", "shadowHidden" FROM "sponsorTimes" `SELECT "startTime", "endTime", "votes", "locked", "UUID", "userID", "category", "videoDuration", "reputation", "shadowHidden" FROM "sponsorTimes"
WHERE "videoID" = ? AND "service" = ? AND "hidden" = 0 ORDER BY "startTime"`, WHERE "videoID" = ? AND "service" = ? AND "hidden" = 0 ORDER BY "startTime"`,
[videoID, service] [videoID, service]
) as Promise<DBSegment[]>; ) as Promise<DBSegment[]>;
@@ -170,7 +171,7 @@ function getWeightedRandomChoice<T extends VotableObject>(choices: T[], amountOf
let choicesWithWeights: TWithWeight[] = choices.map(choice => { let choicesWithWeights: TWithWeight[] = choices.map(choice => {
//The 3 makes -2 the minimum votes before being ignored completely //The 3 makes -2 the minimum votes before being ignored completely
//this can be changed if this system increases in popularity. //this can be changed if this system increases in popularity.
const weight = Math.exp((choice.votes + 3)); const weight = Math.exp((choice.votes + 3 + choice.reputation));
totalWeight += weight; totalWeight += weight;
return {...choice, weight}; return {...choice, weight};
@@ -200,7 +201,7 @@ function getWeightedRandomChoice<T extends VotableObject>(choices: T[], amountOf
//Only one similar time will be returned, randomly generated based on the sqrt of votes. //Only one similar time will be returned, randomly generated based on the sqrt of votes.
//This allows new less voted items to still sometimes appear to give them a chance at getting votes. //This allows new less voted items to still sometimes appear to give them a chance at getting votes.
//Segments with less than -1 votes are already ignored before this function is called //Segments with less than -1 votes are already ignored before this function is called
function chooseSegments(segments: DBSegment[], max: number): DBSegment[] { async function chooseSegments(segments: DBSegment[], max: number): Promise<DBSegment[]> {
//Create groups of segments that are similar to eachother //Create groups of segments that are similar to eachother
//Segments must be sorted by their startTime so that we can build groups chronologically: //Segments must be sorted by their startTime so that we can build groups chronologically:
//1. As long as the segments' startTime fall inside the currentGroup, we keep adding them to that group //1. As long as the segments' startTime fall inside the currentGroup, we keep adding them to that group
@@ -209,9 +210,9 @@ function chooseSegments(segments: DBSegment[], max: number): DBSegment[] {
const overlappingSegmentsGroups: OverlappingSegmentGroup[] = []; const overlappingSegmentsGroups: OverlappingSegmentGroup[] = [];
let currentGroup: OverlappingSegmentGroup; let currentGroup: OverlappingSegmentGroup;
let cursor = -1; //-1 to make sure that, even if the 1st segment starts at 0, a new group is created let cursor = -1; //-1 to make sure that, even if the 1st segment starts at 0, a new group is created
segments.forEach(segment => { for (const segment of segments) {
if (segment.startTime > cursor) { if (segment.startTime > cursor) {
currentGroup = {segments: [], votes: 0, locked: false}; currentGroup = {segments: [], votes: 0, reputation: 0, locked: false};
overlappingSegmentsGroups.push(currentGroup); overlappingSegmentsGroups.push(currentGroup);
} }
@@ -221,17 +222,24 @@ function chooseSegments(segments: DBSegment[], max: number): DBSegment[] {
currentGroup.votes += segment.votes; currentGroup.votes += segment.votes;
} }
segment.reputation = Math.min(segment.reputation, await getReputation(segment.userID));
if (segment.reputation > 0) {
currentGroup.reputation += segment.reputation;
}
if (segment.locked) { if (segment.locked) {
currentGroup.locked = true; currentGroup.locked = true;
} }
cursor = Math.max(cursor, segment.endTime); cursor = Math.max(cursor, segment.endTime);
}); };
overlappingSegmentsGroups.forEach((group) => { overlappingSegmentsGroups.forEach((group) => {
if (group.locked) { if (group.locked) {
group.segments = group.segments.filter((segment) => segment.locked); group.segments = group.segments.filter((segment) => segment.locked);
} }
group.reputation = group.reputation / group.segments.length;
}); });
//if there are too many groups, find the best ones //if there are too many groups, find the best ones

View File

@@ -17,6 +17,7 @@ import { Category, CategoryActionType, IncomingSegment, Segment, SegmentUUID, Se
import { deleteLockCategories } from './deleteLockCategories'; import { deleteLockCategories } from './deleteLockCategories';
import { getCategoryActionType } from '../utils/categoryInfo'; import { getCategoryActionType } from '../utils/categoryInfo';
import { QueryCacher } from '../middleware/queryCacher'; import { QueryCacher } from '../middleware/queryCacher';
import { getReputation } from '../middleware/reputation';
interface APIVideoInfo { interface APIVideoInfo {
err: string | boolean, err: string | boolean,
@@ -508,6 +509,7 @@ export async function postSkipSegments(req: Request, res: Response) {
} }
let startingVotes = 0 + decreaseVotes; let startingVotes = 0 + decreaseVotes;
const reputation = await getReputation(userID);
for (const segmentInfo of segments) { for (const segmentInfo of segments) {
//this can just be a hash of the data //this can just be a hash of the data
@@ -519,9 +521,9 @@ export async function postSkipSegments(req: Request, res: Response) {
const startingLocked = isVIP ? 1 : 0; const startingLocked = isVIP ? 1 : 0;
try { try {
await db.prepare('run', `INSERT INTO "sponsorTimes" await db.prepare('run', `INSERT INTO "sponsorTimes"
("videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "timeSubmitted", "views", "category", "service", "videoDuration", "shadowHidden", "hashedVideoID") ("videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "timeSubmitted", "views", "category", "service", "videoDuration", "reputation", "shadowHidden", "hashedVideoID")
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
videoID, segmentInfo.segment[0], segmentInfo.segment[1], startingVotes, startingLocked, UUID, userID, timeSubmitted, 0, segmentInfo.category, service, videoDuration, shadowBanned, hashedVideoID, videoID, segmentInfo.segment[0], segmentInfo.segment[1], startingVotes, startingLocked, UUID, userID, timeSubmitted, 0, segmentInfo.category, service, videoDuration, reputation, shadowBanned, hashedVideoID,
], ],
); );

View File

@@ -1,5 +1,6 @@
import { HashedValue } from "./hash.model"; import { HashedValue } from "./hash.model";
import { SBRecord } from "./lib.model"; import { SBRecord } from "./lib.model";
import { UserID } from "./user.model";
export type SegmentUUID = string & { __segmentUUIDBrand: unknown }; export type SegmentUUID = string & { __segmentUUIDBrand: unknown };
export type VideoID = string & { __videoIDBrand: unknown }; export type VideoID = string & { __videoIDBrand: unknown };
@@ -42,11 +43,13 @@ export interface DBSegment {
startTime: number; startTime: number;
endTime: number; endTime: number;
UUID: SegmentUUID; UUID: SegmentUUID;
userID: UserID;
votes: number; votes: number;
locked: boolean; locked: boolean;
shadowHidden: Visibility; shadowHidden: Visibility;
videoID: VideoID; videoID: VideoID;
videoDuration: VideoDuration; videoDuration: VideoDuration;
reputation: number;
hashedVideoID: VideoIDHash; hashedVideoID: VideoIDHash;
} }
@@ -54,10 +57,12 @@ export interface OverlappingSegmentGroup {
segments: DBSegment[], segments: DBSegment[],
votes: number; votes: number;
locked: boolean; // Contains a locked segment locked: boolean; // Contains a locked segment
reputation: number;
} }
export interface VotableObject { export interface VotableObject {
votes: number; votes: number;
reputation: number;
} }
export interface VotableObjectWithWeight extends VotableObject { export interface VotableObjectWithWeight extends VotableObject {