diff --git a/src/routes/getSkipSegments.ts b/src/routes/getSkipSegments.ts index 368781d..d4ae698 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, Service, VideoData, VideoID, VideoIDHash, Visibility, VotableObject } from "../types/segments.model"; +import { 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'; @@ -14,7 +14,7 @@ import { getReputation } from '../utils/reputation'; async function prepareCategorySegments(req: Request, videoID: VideoID, category: Category, segments: DBSegment[], cache: SegmentCache = {shadowHiddenSegmentIPs: {}}): Promise { const shouldFilter: boolean[] = await Promise.all(segments.map(async (segment) => { - if (segment.votes < -1) { + if (segment.votes < -1 && !segment.required) { return false; //too untrustworthy, just ignore it } @@ -50,7 +50,7 @@ async function prepareCategorySegments(req: Request, videoID: VideoID, category: })); } -async function getSegmentsByVideoID(req: Request, videoID: VideoID, categories: Category[], service: Service): Promise { +async function getSegmentsByVideoID(req: Request, videoID: VideoID, categories: Category[], requiredSegments: SegmentUUID[], service: Service): Promise { const cache: SegmentCache = {shadowHiddenSegmentIPs: {}}; const segments: Segment[] = []; @@ -61,6 +61,8 @@ async function getSegmentsByVideoID(req: Request, videoID: VideoID, categories: const segmentsByCategory: SBRecord = (await getSegmentsFromDBByVideoID(videoID, service)) .filter((segment: DBSegment) => categories.includes(segment?.category)) .reduce((acc: SBRecord, segment: DBSegment) => { + if (requiredSegments.includes(segment.UUID)) segment.required = true; + acc[segment.category] = acc[segment.category] || []; acc[segment.category].push(segment); @@ -80,7 +82,7 @@ async function getSegmentsByVideoID(req: Request, videoID: VideoID, categories: } } -async function getSegmentsByHash(req: Request, hashedVideoIDPrefix: VideoIDHash, categories: Category[], service: Service): Promise> { +async function getSegmentsByHash(req: Request, hashedVideoIDPrefix: VideoIDHash, categories: Category[], requiredSegments: SegmentUUID[], service: Service): Promise> { const cache: SegmentCache = {shadowHiddenSegmentIPs: {}}; const segments: SBRecord = {}; @@ -97,8 +99,9 @@ async function getSegmentsByHash(req: Request, hashedVideoIDPrefix: VideoIDHash, hash: segment.hashedVideoID, segmentPerCategory: {}, }; - const videoCategories = acc[segment.videoID].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); @@ -214,7 +217,7 @@ async function chooseSegments(segments: DBSegment[], max: number): Promise cursor) { - currentGroup = {segments: [], votes: 0, reputation: 0, locked: false}; + currentGroup = {segments: [], votes: 0, reputation: 0, locked: false, required: false}; overlappingSegmentsGroups.push(currentGroup); } @@ -233,11 +236,18 @@ async function chooseSegments(segments: DBSegment[], max: number): Promise { - if (group.locked) { + if (group.required) { + // Required beats locked + group.segments = group.segments.filter((segment) => segment.required); + } else if (group.locked) { group.segments = group.segments.filter((segment) => segment.locked); } @@ -277,12 +287,24 @@ async function handleGetSegments(req: Request, res: Response): Promise val == service)) { service = Service.YouTube; } - const segments = await getSegmentsByVideoID(req, videoID, categories, service); + const segments = await getSegmentsByVideoID(req, videoID, categories, requiredSegments, service); if (segments === null || segments === undefined) { res.sendStatus(500); diff --git a/src/routes/getSkipSegmentsByHash.ts b/src/routes/getSkipSegmentsByHash.ts index 29194d4..c75d5eb 100644 --- a/src/routes/getSkipSegmentsByHash.ts +++ b/src/routes/getSkipSegmentsByHash.ts @@ -1,7 +1,7 @@ import {hashPrefixTester} from '../utils/hashPrefixTester'; import {getSegmentsByHash} from './getSkipSegments'; import {Request, Response} from 'express'; -import { Category, Service, VideoIDHash } from '../types/segments.model'; +import { Category, SegmentUUID, Service, VideoIDHash } from '../types/segments.model'; export async function getSkipSegmentsByHash(req: Request, res: Response) { let hashPrefix = req.params.prefix as VideoIDHash; @@ -21,11 +21,26 @@ export async function getSkipSegmentsByHash(req: Request, res: Response) { if (!Array.isArray(categories)) { return res.status(400).send("Categories parameter does not match format requirements."); } - } - catch(error) { + } catch(error) { return res.status(400).send("Bad parameter: categories (invalid JSON)"); } + let requiredSegments: SegmentUUID[] = []; + try { + requiredSegments = req.query.requiredSegments + ? JSON.parse(req.query.requiredSegments as string) + : req.query.requiredSegment + ? Array.isArray(req.query.requiredSegment) + ? req.query.requiredSegment + : [req.query.requiredSegment] + : []; + if (!Array.isArray(requiredSegments)) { + return res.status(400).send("requiredSegments parameter does not match format requirements."); + } + } catch(error) { + return res.status(400).send("Bad parameter: requiredSegments (invalid JSON)"); + } + let service: Service = req.query.service ?? req.body.service ?? Service.YouTube; if (!Object.values(Service).some((val) => val == service)) { service = Service.YouTube; @@ -35,7 +50,7 @@ export async function getSkipSegmentsByHash(req: Request, res: Response) { categories = categories.filter((item: any) => typeof item === "string"); // Get all video id's that match hash prefix - const segments = await getSegmentsByHash(req, hashPrefix, categories, service); + const segments = await getSegmentsByHash(req, hashPrefix, categories, requiredSegments, service); if (!segments) return res.status(404).json([]); diff --git a/src/types/segments.model.ts b/src/types/segments.model.ts index 945f294..2fe358c 100644 --- a/src/types/segments.model.ts +++ b/src/types/segments.model.ts @@ -46,6 +46,7 @@ export interface DBSegment { userID: UserID; votes: number; locked: boolean; + required: boolean; // Requested specifically from the client shadowHidden: Visibility; videoID: VideoID; videoDuration: VideoDuration; @@ -57,6 +58,7 @@ export interface OverlappingSegmentGroup { segments: DBSegment[], votes: number; locked: boolean; // Contains a locked segment + required: boolean; // Requested specifically from the client reputation: number; } diff --git a/test/cases/getSkipSegments.ts b/test/cases/getSkipSegments.ts index 7ec0798..d8c951e 100644 --- a/test/cases/getSkipSegments.ts +++ b/test/cases/getSkipSegments.ts @@ -17,6 +17,10 @@ describe('getSkipSegments', () => { await db.prepare("run", query, ['locked', 20, 33, 2, 1, '1-uuid-locked-8', 'testman', 0, 50, 'intro', 'YouTube', 230, 0, 0, getHash('locked', 1)]); await db.prepare("run", query, ['locked', 20, 34, 100000, 0, '1-uuid-9', 'testman', 0, 50, 'intro', 'YouTube', 190, 0, 0, getHash('locked', 1)]); await db.prepare("run", query, ['onlyHiddenSegments', 20, 34, 100000, 0, 'onlyHiddenSegments', 'testman', 0, 50, 'sponsor', 'YouTube', 190, 1, 0, getHash('onlyHiddenSegments', 1)]); + await db.prepare("run", query, ['requiredSegmentVid-raw', 60, 70, 2, 0, 'requiredSegmentVid-raw-1', 'testman', 0, 50, 'sponsor', 'YouTube', 0, 0, 0, getHash('requiredSegmentVid-raw', 1)]); + await db.prepare("run", query, ['requiredSegmentVid-raw', 60, 70, -2, 0, 'requiredSegmentVid-raw-2', 'testman', 0, 50, 'sponsor', 'YouTube', 0, 0, 0, getHash('requiredSegmentVid-raw', 1)]); + await db.prepare("run", query, ['requiredSegmentVid-raw', 80, 90, -2, 0, 'requiredSegmentVid-raw-3', 'testman', 0, 50, 'sponsor', 'YouTube', 0, 0, 0, getHash('requiredSegmentVid-raw', 1)]); + await db.prepare("run", query, ['requiredSegmentVid-raw', 80, 90, 2, 0, 'requiredSegmentVid-raw-4', 'testman', 0, 50, 'sponsor', 'YouTube', 0, 0, 0, getHash('requiredSegmentVid-raw', 1)]); return; }); @@ -309,4 +313,34 @@ describe('getSkipSegments', () => { }) .catch(err => ("Couldn't call endpoint")); }); + + it('Should be able to get specific segments with requiredSegments', (done: Done) => { + fetch(getbaseURL() + '/api/skipSegments?videoID=requiredSegmentVid-raw&requiredSegments=["requiredSegmentVid-raw-2","requiredSegmentVid-raw-3"]') + .then(async res => { + if (res.status !== 200) done("non 200 status code, was " + res.status); + else { + const body = await res.json(); + if (body.length !== 2) done("expected 2 segments, got " + body.length); + else if (body[0].UUID !== 'requiredSegmentVid-raw-2' + || body[1].UUID !== 'requiredSegmentVid-raw-3') done("Did not recieve the correct segments\n" + JSON.stringify(body, null, 2)); + else done(); + } + }) + .catch(err => done("Couldn't call endpoint")); + }); + + it('Should be able to get specific segments with repeating requiredSegment', (done: Done) => { + fetch(getbaseURL() + '/api/skipSegments?videoID=requiredSegmentVid-raw&requiredSegment=requiredSegmentVid-raw-2&requiredSegment=requiredSegmentVid-raw-3') + .then(async res => { + if (res.status !== 200) done("non 200 status code, was " + res.status); + else { + const body = await res.json(); + if (body.length !== 2) done("expected 2 segments, got " + body.length); + else if (body[0].UUID !== 'requiredSegmentVid-raw-2' + || body[1].UUID !== 'requiredSegmentVid-raw-3') done("Did not recieve the correct segments\n" + JSON.stringify(body, null, 2)); + else done(); + } + }) + .catch(err => done("Couldn't call endpoint")); + }); }); diff --git a/test/cases/getSkipSegmentsByHash.ts b/test/cases/getSkipSegmentsByHash.ts index 92cce4d..8625a1d 100644 --- a/test/cases/getSkipSegmentsByHash.ts +++ b/test/cases/getSkipSegmentsByHash.ts @@ -21,6 +21,10 @@ describe('getSegmentsByHash', () => { await db.prepare("run", query, ['onlyHidden', 60, 70, 2, 'onlyHidden', 'testman', 0, 50, 'sponsor', 'YouTube', 1, 0, 'f3a199e1af001d716cdc6599360e2b062c2d2b3fa2885f6d9d2fd741166cbbd3']); await db.prepare("run", query, ['highlightVid', 60, 60, 2, 'highlightVid-1', 'testman', 0, 50, 'highlight', 'YouTube', 0, 0, getHash('highlightVid', 1)]); await db.prepare("run", query, ['highlightVid', 70, 70, 2, 'highlightVid-2', 'testman', 0, 50, 'highlight', 'YouTube', 0, 0, getHash('highlightVid', 1)]); + await db.prepare("run", query, ['requiredSegmentVid', 60, 70, 2, 'requiredSegmentVid-1', 'testman', 0, 50, 'sponsor', 'YouTube', 0, 0, 'd51822c3f681e07aef15a8855f52ad12db9eb9cf059e65b16b64c43359557f61']); + await db.prepare("run", query, ['requiredSegmentVid', 60, 70, -2, 'requiredSegmentVid-2', 'testman', 0, 50, 'sponsor', 'YouTube', 0, 0, 'd51822c3f681e07aef15a8855f52ad12db9eb9cf059e65b16b64c43359557f61']); + await db.prepare("run", query, ['requiredSegmentVid', 80, 90, -2, 'requiredSegmentVid-3', 'testman', 0, 50, 'sponsor', 'YouTube', 0, 0, 'd51822c3f681e07aef15a8855f52ad12db9eb9cf059e65b16b64c43359557f61']); + await db.prepare("run", query, ['requiredSegmentVid', 80, 90, 2, 'requiredSegmentVid-4', 'testman', 0, 50, 'sponsor', 'YouTube', 0, 0, 'd51822c3f681e07aef15a8855f52ad12db9eb9cf059e65b16b64c43359557f61']); }); it('Should be able to get a 200', (done: Done) => { @@ -219,4 +223,36 @@ describe('getSegmentsByHash', () => { }) .catch(err => done('(post) ' + err)); }); + + it('Should be able to get specific segments with requiredSegments', (done: Done) => { + fetch(getbaseURL() + '/api/skipSegments/d518?requiredSegments=["requiredSegmentVid-2","requiredSegmentVid-3"]') + .then(async res => { + if (res.status !== 200) done("non 200 status code, was " + res.status); + else { + const body = await res.json(); + if (body.length !== 1) done("expected 1 video, got " + body.length); + else if (body[0].segments.length !== 2) done("expected 2 segments for video, got " + body[0].segments.length); + else if (body[0].segments[0].UUID !== 'requiredSegmentVid-2' + || body[0].segments[1].UUID !== 'requiredSegmentVid-3') done("Did not recieve the correct segments\n" + JSON.stringify(body, null, 2)); + else done(); + } + }) + .catch(err => done("Couldn't call endpoint")); + }); + + it('Should be able to get specific segments with repeating requiredSegment', (done: Done) => { + fetch(getbaseURL() + '/api/skipSegments/d518?requiredSegment=requiredSegmentVid-2&requiredSegment=requiredSegmentVid-3') + .then(async res => { + if (res.status !== 200) done("non 200 status code, was " + res.status); + else { + const body = await res.json(); + if (body.length !== 1) done("expected 1 video, got " + body.length); + else if (body[0].segments.length !== 2) done("expected 2 segments for video, got " + body[0].segments.length); + else if (body[0].segments[0].UUID !== 'requiredSegmentVid-2' + || body[0].segments[1].UUID !== 'requiredSegmentVid-3') done("Did not recieve the correct segments\n" + JSON.stringify(body, null, 2)); + else done(); + } + }) + .catch(err => done("Couldn't call endpoint")); + }); });