Merge pull request #298 from ajayyy/mute-skip

Silent skip type
This commit is contained in:
Ajay Ramachandran
2021-07-06 01:00:22 -04:00
committed by GitHub
9 changed files with 278 additions and 55 deletions

View File

@@ -3,7 +3,7 @@ import { config } from '../config';
import { db, privateDB } from '../databases/databases';
import { skipSegmentsHashKey, skipSegmentsKey } from '../utils/redisKeys';
import { SBRecord } from '../types/lib.model';
import { Category, CategoryActionType, DBSegment, HashedIP, IPAddress, OverlappingSegmentGroup, Segment, SegmentCache, SegmentUUID, Service, VideoData, VideoID, VideoIDHash, Visibility, VotableObject } from "../types/segments.model";
import { ActionType, Category, CategoryActionType, DBSegment, HashedIP, IPAddress, OverlappingSegmentGroup, Segment, SegmentCache, SegmentUUID, Service, VideoData, VideoID, VideoIDHash, Visibility, VotableObject } from "../types/segments.model";
import { getCategoryActionType } from '../utils/categoryInfo';
import { getHash } from '../utils/getHash';
import { getIP } from '../utils/getIP';
@@ -12,7 +12,7 @@ import { QueryCacher } from '../utils/queryCacher';
import { getReputation } from '../utils/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[]> {
const shouldFilter: boolean[] = await Promise.all(segments.map(async (segment) => {
if (segment.votes < -1 && !segment.required) {
return false; //too untrustworthy, just ignore it
@@ -43,14 +43,16 @@ async function prepareCategorySegments(req: Request, videoID: VideoID, category:
const maxSegments = getCategoryActionType(category) === CategoryActionType.Skippable ? 32 : 1;
return (await chooseSegments(filteredSegments, maxSegments)).map((chosenSegment) => ({
category,
category: chosenSegment.category,
actionType: chosenSegment.actionType,
segment: [chosenSegment.startTime, chosenSegment.endTime],
UUID: chosenSegment.UUID,
videoDuration: chosenSegment.videoDuration
}));
}
async function getSegmentsByVideoID(req: Request, videoID: VideoID, categories: Category[], requiredSegments: SegmentUUID[], service: Service): Promise<Segment[]> {
async function getSegmentsByVideoID(req: Request, videoID: VideoID, categories: Category[],
actionTypes: ActionType[], requiredSegments: SegmentUUID[], service: Service): Promise<Segment[]> {
const cache: SegmentCache = {shadowHiddenSegmentIPs: {}};
const segments: Segment[] = [];
@@ -59,16 +61,17 @@ async function getSegmentsByVideoID(req: Request, videoID: VideoID, categories:
if (categories.length === 0) return null;
const segmentsByCategory: SBRecord<Category, DBSegment[]> = (await getSegmentsFromDBByVideoID(videoID, service))
.filter((segment: DBSegment) => categories.includes(segment?.category))
.filter((segment: DBSegment) => categories.includes(segment?.category) && actionTypes.includes(segment?.actionType))
.reduce((acc: SBRecord<Category, DBSegment[]>, segment: DBSegment) => {
if (requiredSegments.includes(segment.UUID)) segment.required = true;
acc[segment.category] = acc[segment.category] || [];
acc[segment.category] ??= [];
acc[segment.category].push(segment);
return acc;
}, {});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const [category, categorySegments] of Object.entries(segmentsByCategory)) {
segments.push(...(await prepareCategorySegments(req, videoID, category as Category, categorySegments, cache)));
}
@@ -82,7 +85,8 @@ async function getSegmentsByVideoID(req: Request, videoID: VideoID, categories:
}
}
async function getSegmentsByHash(req: Request, hashedVideoIDPrefix: VideoIDHash, categories: Category[], requiredSegments: SegmentUUID[], service: Service): Promise<SBRecord<VideoID, VideoData>> {
async function getSegmentsByHash(req: Request, hashedVideoIDPrefix: VideoIDHash, categories: Category[],
actionTypes: ActionType[], requiredSegments: SegmentUUID[], service: Service): Promise<SBRecord<VideoID, VideoData>> {
const cache: SegmentCache = {shadowHiddenSegmentIPs: {}};
const segments: SBRecord<VideoID, VideoData> = {};
@@ -93,17 +97,16 @@ async function getSegmentsByHash(req: Request, hashedVideoIDPrefix: VideoIDHash,
if (categories.length === 0) return null;
const segmentPerVideoID: SegmentWithHashPerVideoID = (await getSegmentsFromDBByHash(hashedVideoIDPrefix, service))
.filter((segment: DBSegment) => categories.includes(segment?.category))
.filter((segment: DBSegment) => categories.includes(segment?.category) && actionTypes.includes(segment?.actionType))
.reduce((acc: SegmentWithHashPerVideoID, segment: DBSegment) => {
acc[segment.videoID] = acc[segment.videoID] || {
hash: segment.hashedVideoID,
segmentPerCategory: {},
segmentPerCategory: {}
};
if (requiredSegments.includes(segment.UUID)) segment.required = true;
const videoCategories = acc[segment.videoID].segmentPerCategory;
videoCategories[segment.category] = videoCategories[segment.category] || [];
videoCategories[segment.category].push(segment);
acc[segment.videoID].segmentPerCategory[segment.category] ??= [];
acc[segment.videoID].segmentPerCategory[segment.category].push(segment);
return acc;
}, {});
@@ -114,6 +117,7 @@ async function getSegmentsByHash(req: Request, hashedVideoIDPrefix: VideoIDHash,
segments: [],
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const [category, segmentPerCategory] of Object.entries(videoData.segmentPerCategory)) {
segments[videoID].segments.push(...(await prepareCategorySegments(req, videoID as VideoID, category as Category, segmentPerCategory, cache)));
}
@@ -132,7 +136,7 @@ async function getSegmentsFromDBByHash(hashedVideoIDPrefix: VideoIDHash, service
const fetchFromDB = () => db
.prepare(
'all',
`SELECT "videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "category", "videoDuration", "reputation", "shadowHidden", "hashedVideoID" FROM "sponsorTimes"
`SELECT "videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "category", "actionType", "videoDuration", "reputation", "shadowHidden", "hashedVideoID" FROM "sponsorTimes"
WHERE "hashedVideoID" LIKE ? AND "service" = ? AND "hidden" = 0 ORDER BY "startTime"`,
[hashedVideoIDPrefix + '%', service]
) as Promise<DBSegment[]>;
@@ -148,7 +152,7 @@ async function getSegmentsFromDBByVideoID(videoID: VideoID, service: Service): P
const fetchFromDB = () => db
.prepare(
'all',
`SELECT "startTime", "endTime", "votes", "locked", "UUID", "userID", "category", "videoDuration", "reputation", "shadowHidden" FROM "sponsorTimes"
`SELECT "startTime", "endTime", "votes", "locked", "UUID", "userID", "category", "actionType", "videoDuration", "reputation", "shadowHidden" FROM "sponsorTimes"
WHERE "videoID" = ? AND "service" = ? AND "hidden" = 0 ORDER BY "startTime"`,
[videoID, service]
) as Promise<DBSegment[]>;
@@ -275,7 +279,7 @@ async function handleGetSegments(req: Request, res: Response): Promise<Segment[]
const videoID = req.query.videoID as VideoID;
// Default to sponsor
// If using params instead of JSON, only one category can be pulled
const categories = req.query.categories
const categories: Category[] = req.query.categories
? JSON.parse(req.query.categories as string)
: req.query.category
? Array.isArray(req.query.category)
@@ -287,6 +291,18 @@ async function handleGetSegments(req: Request, res: Response): Promise<Segment[]
return false;
}
const actionTypes: ActionType[] = req.query.actionTypes
? JSON.parse(req.query.actionTypes as string)
: req.query.actionType
? Array.isArray(req.query.actionType)
? req.query.actionType
: [req.query.actionType]
: [ActionType.Skip];
if (!Array.isArray(actionTypes)) {
res.status(400).send("actionTypes parameter does not match format requirements.");
return false;
}
const requiredSegments: SegmentUUID[] = req.query.requiredSegments
? JSON.parse(req.query.requiredSegments as string)
: req.query.requiredSegment
@@ -304,7 +320,7 @@ async function handleGetSegments(req: Request, res: Response): Promise<Segment[]
service = Service.YouTube;
}
const segments = await getSegmentsByVideoID(req, videoID, categories, requiredSegments, service);
const segments = await getSegmentsByVideoID(req, videoID, categories, actionTypes, requiredSegments, service);
if (segments === null || segments === undefined) {
res.sendStatus(500);

View File

@@ -1,7 +1,7 @@
import {hashPrefixTester} from '../utils/hashPrefixTester';
import {getSegmentsByHash} from './getSkipSegments';
import {Request, Response} from 'express';
import { Category, SegmentUUID, Service, VideoIDHash } from '../types/segments.model';
import { ActionType, Category, SegmentUUID, Service, VideoIDHash } from '../types/segments.model';
export async function getSkipSegmentsByHash(req: Request, res: Response): Promise<Response> {
let hashPrefix = req.params.prefix as VideoIDHash;
@@ -26,6 +26,22 @@ export async function getSkipSegmentsByHash(req: Request, res: Response): Promis
return res.status(400).send("Bad parameter: categories (invalid JSON)");
}
let actionTypes: ActionType[] = [];
try {
actionTypes = req.query.actionTypes
? JSON.parse(req.query.actionTypes as string)
: req.query.actionType
? Array.isArray(req.query.actionType)
? req.query.actionType
: [req.query.actionType]
: [ActionType.Skip];
if (!Array.isArray(actionTypes)) {
return res.status(400).send("actionTypes parameter does not match format requirements.");
}
} catch(error) {
return res.status(400).send("Bad parameter: actionTypes (invalid JSON)");
}
let requiredSegments: SegmentUUID[] = [];
try {
requiredSegments = req.query.requiredSegments
@@ -51,7 +67,7 @@ export async function getSkipSegmentsByHash(req: Request, res: Response): Promis
categories = categories.filter((item: any) => typeof item === "string");
// Get all video id's that match hash prefix
const segments = await getSegmentsByHash(req, hashPrefix, categories, requiredSegments, service);
const segments = await getSegmentsByHash(req, hashPrefix, categories, actionTypes, requiredSegments, service);
if (!segments) return res.status(404).json([]);

View File

@@ -10,7 +10,7 @@ import {getFormattedTime} from '../utils/getFormattedTime';
import {isUserTrustworthy} from '../utils/isUserTrustworthy';
import {dispatchEvent} from '../utils/webhookUtils';
import {Request, Response} from 'express';
import { Category, CategoryActionType, IncomingSegment, SegmentUUID, Service, VideoDuration, VideoID } from '../types/segments.model';
import { ActionType, Category, CategoryActionType, IncomingSegment, SegmentUUID, Service, VideoDuration, VideoID } from '../types/segments.model';
import { deleteLockCategories } from './deleteLockCategories';
import { getCategoryActionType } from '../utils/categoryInfo';
import { QueryCacher } from '../utils/queryCacher';
@@ -315,7 +315,7 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
const videoID = req.query.videoID || req.body.videoID;
let userID = req.query.userID || req.body.userID;
let service: Service = req.query.service ?? req.body.service ?? Service.YouTube;
if (!Object.values(Service).some((val) => val == service)) {
if (!Object.values(Service).some((val) => val === service)) {
service = Service.YouTube;
}
let videoDuration: VideoDuration = (parseFloat(req.query.videoDuration || req.body.videoDuration) || 0) as VideoDuration;
@@ -325,9 +325,16 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
// Use query instead
segments = [{
segment: [req.query.startTime as string, req.query.endTime as string],
category: req.query.category as Category
category: req.query.category as Category,
actionType: (req.query.actionType as ActionType) ?? ActionType.Skip
}];
}
// Add default action type
segments.forEach((segment) => {
if (!Object.values(ActionType).some((val) => val === segment.actionType)){
segment.actionType = ActionType.Skip;
}
});
const invalidFields = [];
if (typeof videoID !== 'string') {
@@ -518,9 +525,9 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
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", "reputation", "shadowHidden", "hashedVideoID")
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
videoID, segmentInfo.segment[0], segmentInfo.segment[1], startingVotes, startingLocked, UUID, userID, timeSubmitted, 0, segmentInfo.category, service, videoDuration, reputation, shadowBanned, hashedVideoID,
("videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "timeSubmitted", "views", "category", "actionType", "service", "videoDuration", "reputation", "shadowHidden", "hashedVideoID")
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
videoID, segmentInfo.segment[0], segmentInfo.segment[1], startingVotes, startingLocked, UUID, userID, timeSubmitted, 0, segmentInfo.category, segmentInfo.actionType, service, videoDuration, reputation, shadowBanned, hashedVideoID,
],
);