Add parameter to fetch specific segments

This commit is contained in:
Ajay Ramachandran
2021-07-04 14:15:17 -04:00
parent be277d0218
commit 990572ff31
5 changed files with 121 additions and 12 deletions

View File

@@ -3,7 +3,7 @@ import { config } from '../config';
import { db, privateDB } from '../databases/databases'; import { db, privateDB } from '../databases/databases';
import { skipSegmentsHashKey, skipSegmentsKey } from '../utils/redisKeys'; import { skipSegmentsHashKey, skipSegmentsKey } from '../utils/redisKeys';
import { SBRecord } from '../types/lib.model'; 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 { getCategoryActionType } from '../utils/categoryInfo';
import { getHash } from '../utils/getHash'; import { getHash } from '../utils/getHash';
import { getIP } from '../utils/getIP'; 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<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) => { 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 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<Segment[]> { async function getSegmentsByVideoID(req: Request, videoID: VideoID, categories: Category[], requiredSegments: SegmentUUID[], service: Service): Promise<Segment[]> {
const cache: SegmentCache = {shadowHiddenSegmentIPs: {}}; const cache: SegmentCache = {shadowHiddenSegmentIPs: {}};
const segments: Segment[] = []; const segments: Segment[] = [];
@@ -61,6 +61,8 @@ async function getSegmentsByVideoID(req: Request, videoID: VideoID, categories:
const segmentsByCategory: SBRecord<Category, DBSegment[]> = (await getSegmentsFromDBByVideoID(videoID, service)) const segmentsByCategory: SBRecord<Category, DBSegment[]> = (await getSegmentsFromDBByVideoID(videoID, service))
.filter((segment: DBSegment) => categories.includes(segment?.category)) .filter((segment: DBSegment) => categories.includes(segment?.category))
.reduce((acc: SBRecord<Category, DBSegment[]>, segment: DBSegment) => { .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] || [];
acc[segment.category].push(segment); 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<SBRecord<VideoID, VideoData>> { async function getSegmentsByHash(req: Request, hashedVideoIDPrefix: VideoIDHash, categories: Category[], requiredSegments: SegmentUUID[], service: Service): Promise<SBRecord<VideoID, VideoData>> {
const cache: SegmentCache = {shadowHiddenSegmentIPs: {}}; const cache: SegmentCache = {shadowHiddenSegmentIPs: {}};
const segments: SBRecord<VideoID, VideoData> = {}; const segments: SBRecord<VideoID, VideoData> = {};
@@ -97,8 +99,9 @@ async function getSegmentsByHash(req: Request, hashedVideoIDPrefix: VideoIDHash,
hash: segment.hashedVideoID, hash: segment.hashedVideoID,
segmentPerCategory: {}, 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] = videoCategories[segment.category] || [];
videoCategories[segment.category].push(segment); videoCategories[segment.category].push(segment);
@@ -214,7 +217,7 @@ async function chooseSegments(segments: DBSegment[], max: number): Promise<DBSeg
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
for (const segment of segments) { for (const segment of segments) {
if (segment.startTime > cursor) { if (segment.startTime > cursor) {
currentGroup = {segments: [], votes: 0, reputation: 0, locked: false}; currentGroup = {segments: [], votes: 0, reputation: 0, locked: false, required: false};
overlappingSegmentsGroups.push(currentGroup); overlappingSegmentsGroups.push(currentGroup);
} }
@@ -233,11 +236,18 @@ async function chooseSegments(segments: DBSegment[], max: number): Promise<DBSeg
currentGroup.locked = true; currentGroup.locked = true;
} }
if (segment.required) {
currentGroup.required = true;
}
cursor = Math.max(cursor, segment.endTime); cursor = Math.max(cursor, segment.endTime);
}; };
overlappingSegmentsGroups.forEach((group) => { overlappingSegmentsGroups.forEach((group) => {
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); group.segments = group.segments.filter((segment) => segment.locked);
} }
@@ -277,12 +287,24 @@ async function handleGetSegments(req: Request, res: Response): Promise<Segment[]
return false; return false;
} }
const requiredSegments: SegmentUUID[] = 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)) {
res.status(400).send("requiredSegments parameter does not match format requirements.");
return false;
}
let service: Service = req.query.service ?? req.body.service ?? Service.YouTube; 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; 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) { if (segments === null || segments === undefined) {
res.sendStatus(500); res.sendStatus(500);

View File

@@ -1,7 +1,7 @@
import {hashPrefixTester} from '../utils/hashPrefixTester'; import {hashPrefixTester} from '../utils/hashPrefixTester';
import {getSegmentsByHash} from './getSkipSegments'; import {getSegmentsByHash} from './getSkipSegments';
import {Request, Response} from 'express'; 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) { export async function getSkipSegmentsByHash(req: Request, res: Response) {
let hashPrefix = req.params.prefix as VideoIDHash; let hashPrefix = req.params.prefix as VideoIDHash;
@@ -21,11 +21,26 @@ export async function getSkipSegmentsByHash(req: Request, res: Response) {
if (!Array.isArray(categories)) { if (!Array.isArray(categories)) {
return res.status(400).send("Categories parameter does not match format requirements."); 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)"); 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; 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; service = Service.YouTube;
@@ -35,7 +50,7 @@ export async function getSkipSegmentsByHash(req: Request, res: Response) {
categories = categories.filter((item: any) => typeof item === "string"); categories = categories.filter((item: any) => typeof item === "string");
// Get all video id's that match hash prefix // 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([]); if (!segments) return res.status(404).json([]);

View File

@@ -46,6 +46,7 @@ export interface DBSegment {
userID: UserID; userID: UserID;
votes: number; votes: number;
locked: boolean; locked: boolean;
required: boolean; // Requested specifically from the client
shadowHidden: Visibility; shadowHidden: Visibility;
videoID: VideoID; videoID: VideoID;
videoDuration: VideoDuration; videoDuration: VideoDuration;
@@ -57,6 +58,7 @@ export interface OverlappingSegmentGroup {
segments: DBSegment[], segments: DBSegment[],
votes: number; votes: number;
locked: boolean; // Contains a locked segment locked: boolean; // Contains a locked segment
required: boolean; // Requested specifically from the client
reputation: number; reputation: number;
} }

View File

@@ -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, 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, ['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, ['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; return;
}); });
@@ -309,4 +313,34 @@ describe('getSkipSegments', () => {
}) })
.catch(err => ("Couldn't call endpoint")); .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"));
});
}); });

View File

@@ -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, ['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', 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, ['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) => { it('Should be able to get a 200', (done: Done) => {
@@ -219,4 +223,36 @@ describe('getSegmentsByHash', () => {
}) })
.catch(err => done('(post) ' + err)); .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"));
});
}); });