diff --git a/src/routes/getSkipSegments.ts b/src/routes/getSkipSegments.ts index 8f78b5d..d671b00 100644 --- a/src/routes/getSkipSegments.ts +++ b/src/routes/getSkipSegments.ts @@ -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 { +async function prepareCategorySegments(req: Request, videoID: VideoID, segments: DBSegment[], cache: SegmentCache = {shadowHiddenSegmentIPs: {}}): Promise { const shouldFilter: boolean[] = await Promise.all(segments.map(async (segment) => { if (segment.votes < -1 && !segment.required) { return false; //too untrustworthy, just ignore it @@ -41,16 +41,18 @@ async function prepareCategorySegments(req: Request, videoID: VideoID, category: const filteredSegments = segments.filter((_, index) => shouldFilter[index]); - const maxSegments = getCategoryActionType(category) === CategoryActionType.Skippable ? 32 : 1; + const maxSegments = getCategoryActionType(segments[0]?.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 { +async function getSegmentsByVideoID(req: Request, videoID: VideoID, categories: Category[], + actionTypes: ActionType[], requiredSegments: SegmentUUID[], service: Service): Promise { const cache: SegmentCache = {shadowHiddenSegmentIPs: {}}; const segments: Segment[] = []; @@ -58,19 +60,20 @@ async function getSegmentsByVideoID(req: Request, videoID: VideoID, categories: categories = categories.filter((category) => !/[^a-z|_|-]/.test(category)); if (categories.length === 0) return null; - const segmentsByCategory: SBRecord = (await getSegmentsFromDBByVideoID(videoID, service)) - .filter((segment: DBSegment) => categories.includes(segment?.category)) + const segmentsByType: SBRecord = (await getSegmentsFromDBByVideoID(videoID, service)) + .filter((segment: DBSegment) => categories.includes(segment?.category) && actionTypes.includes(segment?.actionType)) .reduce((acc: SBRecord, segment: DBSegment) => { if (requiredSegments.includes(segment.UUID)) segment.required = true; - acc[segment.category] = acc[segment.category] || []; - acc[segment.category].push(segment); + acc[segment.category + segment.actionType] ??= []; + acc[segment.category + segment.actionType].push(segment); return acc; }, {}); - for (const [category, categorySegments] of Object.entries(segmentsByCategory)) { - segments.push(...(await prepareCategorySegments(req, videoID, category as Category, categorySegments, cache))); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for (const [_key, typeSegments] of Object.entries(segmentsByType)) { + segments.push(...(await prepareCategorySegments(req, videoID, typeSegments, cache))); } return segments; @@ -82,28 +85,28 @@ async function getSegmentsByVideoID(req: Request, videoID: VideoID, categories: } } -async function getSegmentsByHash(req: Request, hashedVideoIDPrefix: VideoIDHash, categories: Category[], requiredSegments: SegmentUUID[], service: Service): Promise> { +async function getSegmentsByHash(req: Request, hashedVideoIDPrefix: VideoIDHash, categories: Category[], + actionTypes: ActionType[], requiredSegments: SegmentUUID[], service: Service): Promise> { const cache: SegmentCache = {shadowHiddenSegmentIPs: {}}; const segments: SBRecord = {}; try { - type SegmentWithHashPerVideoID = SBRecord}>; + type SegmentWithHashPerVideoID = SBRecord}>; categories = categories.filter((category) => !(/[^a-z|_|-]/.test(category))); 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: {}, + segmentPerType: {} }; 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].segmentPerType[segment.category + segment.actionType] ??= []; + acc[segment.videoID].segmentPerType[segment.category + segment.actionType].push(segment); return acc; }, {}); @@ -114,8 +117,9 @@ async function getSegmentsByHash(req: Request, hashedVideoIDPrefix: VideoIDHash, segments: [], }; - for (const [category, segmentPerCategory] of Object.entries(videoData.segmentPerCategory)) { - segments[videoID].segments.push(...(await prepareCategorySegments(req, videoID as VideoID, category as Category, segmentPerCategory, cache))); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for (const [_key, segmentPerType] of Object.entries(videoData.segmentPerType)) { + segments[videoID].segments.push(...(await prepareCategorySegments(req, videoID as VideoID, segmentPerType, 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; @@ -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; @@ -275,7 +279,7 @@ async function handleGetSegments(req: Request, res: Response): Promise { 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([]); diff --git a/src/types/segments.model.ts b/src/types/segments.model.ts index 98abf4b..9d72829 100644 --- a/src/types/segments.model.ts +++ b/src/types/segments.model.ts @@ -34,6 +34,7 @@ export interface IncomingSegment { export interface Segment { category: Category; + actionType: ActionType; segment: number[]; UUID: SegmentUUID; videoDuration: VideoDuration; @@ -46,6 +47,7 @@ export enum Visibility { export interface DBSegment { category: Category; + actionType: ActionType; startTime: number; endTime: number; UUID: SegmentUUID; diff --git a/src/utils/redisKeys.ts b/src/utils/redisKeys.ts index fde37f3..fe37691 100644 --- a/src/utils/redisKeys.ts +++ b/src/utils/redisKeys.ts @@ -3,14 +3,14 @@ import { UserID } from "../types/user.model"; import { Logger } from "./logger"; export function skipSegmentsKey(videoID: VideoID, service: Service): string { - return "segments." + service + ".videoID." + videoID; + return "segments.v2." + service + ".videoID." + 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; + return "segments.v2." + service + "." + hashedVideoIDPrefix; } export function reputationKey(userID: UserID): string {