Merge pull request #224 from ajayyy/export

Add highlight category
This commit is contained in:
Ajay Ramachandran
2021-05-23 11:22:02 -04:00
committed by GitHub
15 changed files with 303 additions and 192 deletions

View File

@@ -22,7 +22,7 @@
"mode": "development", "mode": "development",
"readOnly": false, "readOnly": false,
"webhooks": [], "webhooks": [],
"categoryList": ["sponsor", "intro", "outro", "interaction", "selfpromo", "music_offtopic"], // List of supported categories any other category will be rejected "categoryList": ["sponsor", "intro", "outro", "interaction", "selfpromo", "preview", "music_offtopic", "highlight"], // List of supported categories any other category will be rejected
"getTopUsersCacheTimeMinutes": 5, // cacheTime for getTopUsers result in minutes "getTopUsersCacheTimeMinutes": 5, // cacheTime for getTopUsers result in minutes
"maxNumberOfActiveWarnings": 3, // Users with this number of warnings will be blocked until warnings expire "maxNumberOfActiveWarnings": 3, // Users with this number of warnings will be blocked until warnings expire
"hoursAfterWarningExpire": 24, "hoursAfterWarningExpire": 24,

View File

@@ -16,7 +16,7 @@ addDefaults(config, {
privateDBSchema: "./databases/_private.db.sql", privateDBSchema: "./databases/_private.db.sql",
readOnly: false, readOnly: false,
webhooks: [], webhooks: [],
categoryList: ["sponsor", "selfpromo", "interaction", "intro", "outro", "preview", "music_offtopic"], categoryList: ["sponsor", "selfpromo", "interaction", "intro", "outro", "preview", "music_offtopic", "highlight"],
maxNumberOfActiveWarnings: 3, maxNumberOfActiveWarnings: 3,
hoursAfterWarningExpires: 24, hoursAfterWarningExpires: 24,
adminUserID: "", adminUserID: "",

View File

@@ -4,7 +4,8 @@ import { config } from '../config';
import { db, privateDB } from '../databases/databases'; import { db, privateDB } from '../databases/databases';
import { skipSegmentsHashKey, skipSegmentsKey } from '../middleware/redisKeys'; import { skipSegmentsHashKey, skipSegmentsKey } from '../middleware/redisKeys';
import { SBRecord } from '../types/lib.model'; import { SBRecord } from '../types/lib.model';
import { Category, 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, Service, VideoData, VideoID, VideoIDHash, Visibility, VotableObject } from "../types/segments.model";
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';
import { Logger } from '../utils/logger'; import { Logger } from '../utils/logger';
@@ -40,7 +41,8 @@ async function prepareCategorySegments(req: Request, videoID: VideoID, category:
const filteredSegments = segments.filter((_, index) => shouldFilter[index]); const filteredSegments = segments.filter((_, index) => shouldFilter[index]);
return chooseSegments(filteredSegments).map((chosenSegment) => ({ const maxSegments = getCategoryActionType(category) === CategoryActionType.Skippable ? 32 : 1
return chooseSegments(filteredSegments, maxSegments).map((chosenSegment) => ({
category, category,
segment: [chosenSegment.startTime, chosenSegment.endTime], segment: [chosenSegment.startTime, chosenSegment.endTime],
UUID: chosenSegment.UUID, UUID: chosenSegment.UUID,
@@ -206,7 +208,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[]): DBSegment[] { function chooseSegments(segments: DBSegment[], max: number): 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
@@ -240,8 +242,8 @@ function chooseSegments(segments: DBSegment[]): DBSegment[] {
} }
}); });
//if there are too many groups, find the best 8 //if there are too many groups, find the best ones
return getWeightedRandomChoice(overlappingSegmentsGroups, 32).map( return getWeightedRandomChoice(overlappingSegmentsGroups, max).map(
//randomly choose 1 good segment per group and return them //randomly choose 1 good segment per group and return them
group => getWeightedRandomChoice(group.segments, 1)[0], group => getWeightedRandomChoice(group.segments, 1)[0],
); );

View File

@@ -4,7 +4,7 @@ import {db, privateDB} from '../databases/databases';
import {YouTubeAPI} from '../utils/youtubeApi'; import {YouTubeAPI} from '../utils/youtubeApi';
import {getSubmissionUUID} from '../utils/getSubmissionUUID'; import {getSubmissionUUID} from '../utils/getSubmissionUUID';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import isoDurations from 'iso8601-duration'; import isoDurations, { end } from 'iso8601-duration';
import {getHash} from '../utils/getHash'; import {getHash} from '../utils/getHash';
import {getIP} from '../utils/getIP'; import {getIP} from '../utils/getIP';
import {getFormattedTime} from '../utils/getFormattedTime'; import {getFormattedTime} from '../utils/getFormattedTime';
@@ -13,12 +13,13 @@ import {dispatchEvent} from '../utils/webhookUtils';
import {Request, Response} from 'express'; import {Request, Response} from 'express';
import { skipSegmentsHashKey, skipSegmentsKey } from '../middleware/redisKeys'; import { skipSegmentsHashKey, skipSegmentsKey } from '../middleware/redisKeys';
import redis from '../utils/redis'; import redis from '../utils/redis';
import { Category, IncomingSegment, Segment, SegmentUUID, Service, VideoDuration, VideoID } from '../types/segments.model'; import { Category, CategoryActionType, IncomingSegment, Segment, SegmentUUID, Service, VideoDuration, VideoID } from '../types/segments.model';
import { deleteLockCategories } from './deleteLockCategories'; import { deleteLockCategories } from './deleteLockCategories';
import { getCategoryActionType } from '../utils/categoryInfo';
interface APIVideoInfo { interface APIVideoInfo {
err: string | boolean, err: string | boolean,
data: any data?: any
} }
async function sendWebhookNotification(userID: string, videoID: string, UUID: string, submissionCount: number, youtubeData: any, {submissionStart, submissionEnd}: { submissionStart: number; submissionEnd: number; }, segmentInfo: any) { async function sendWebhookNotification(userID: string, videoID: string, UUID: string, submissionCount: number, youtubeData: any, {submissionStart, submissionEnd}: { submissionStart: number; submissionEnd: number; }, segmentInfo: any) {
@@ -275,11 +276,9 @@ function getYouTubeVideoDuration(apiVideoInfo: APIVideoInfo): VideoDuration {
return duration ? isoDurations.toSeconds(isoDurations.parse(duration)) as VideoDuration : null; return duration ? isoDurations.toSeconds(isoDurations.parse(duration)) as VideoDuration : null;
} }
async function getYouTubeVideoInfo(videoID: VideoID): Promise<APIVideoInfo> { async function getYouTubeVideoInfo(videoID: VideoID, ignoreCache = false): Promise<APIVideoInfo> {
if (config.youtubeAPIKey !== null) { if (config.youtubeAPIKey !== null) {
return new Promise((resolve) => { return YouTubeAPI.listVideos(videoID, ignoreCache);
YouTubeAPI.listVideos(videoID, (err: any, data: any) => resolve({err, data}));
});
} else { } else {
return null; return null;
} }
@@ -367,9 +366,16 @@ export async function postSkipSegments(req: Request, res: Response) {
const decreaseVotes = 0; const decreaseVotes = 0;
const previousSubmissions = await db.prepare('all', `SELECT "videoDuration", "UUID" FROM "sponsorTimes" WHERE "videoID" = ? AND "service" = ? AND "hidden" = 0
AND "shadowHidden" = 0 AND "votes" >= 0 AND "videoDuration" != 0`, [videoID, service]) as
{videoDuration: VideoDuration, UUID: SegmentUUID}[];
// If the video's duration is changed, then the video should be unlocked and old submissions should be hidden
const videoDurationChanged = (videoDuration: number) => previousSubmissions.length > 0 && !previousSubmissions.some((e) => Math.abs(videoDuration - e.videoDuration) < 2);
let apiVideoInfo: APIVideoInfo = null; let apiVideoInfo: APIVideoInfo = null;
if (service == Service.YouTube) { if (service == Service.YouTube) {
apiVideoInfo = await getYouTubeVideoInfo(videoID); // Don't use cache if we don't know the video duraton, or the client claims that it has changed
apiVideoInfo = await getYouTubeVideoInfo(videoID, !videoDuration || videoDurationChanged(videoDuration));
} }
const apiVideoDuration = getYouTubeVideoDuration(apiVideoInfo); const apiVideoDuration = getYouTubeVideoDuration(apiVideoInfo);
if (!videoDuration || (apiVideoDuration && Math.abs(videoDuration - apiVideoDuration) > 2)) { if (!videoDuration || (apiVideoDuration && Math.abs(videoDuration - apiVideoDuration) > 2)) {
@@ -377,12 +383,7 @@ export async function postSkipSegments(req: Request, res: Response) {
videoDuration = apiVideoDuration || 0 as VideoDuration; videoDuration = apiVideoDuration || 0 as VideoDuration;
} }
const previousSubmissions = await db.prepare('all', `SELECT "videoDuration", "UUID" FROM "sponsorTimes" WHERE "videoID" = ? AND "service" = ? AND "hidden" = 0 if (videoDurationChanged(videoDuration)) {
AND "shadowHidden" = 0 AND "votes" >= 0 AND "videoDuration" != 0`, [videoID, service]) as
{videoDuration: VideoDuration, UUID: SegmentUUID}[];
// If the video's duration is changed, then the video should be unlocked and old submissions should be hidden
const videoDurationChanged = previousSubmissions.length > 0 && !previousSubmissions.some((e) => Math.abs(videoDuration - e.videoDuration) < 2);
if (videoDurationChanged) {
// Hide all previous submissions // Hide all previous submissions
for (const submission of previousSubmissions) { for (const submission of previousSubmissions) {
await db.prepare('run', `UPDATE "sponsorTimes" SET "hidden" = 1 WHERE "UUID" = ?`, [submission.UUID]); await db.prepare('run', `UPDATE "sponsorTimes" SET "hidden" = 1 WHERE "UUID" = ?`, [submission.UUID]);
@@ -411,10 +412,10 @@ export async function postSkipSegments(req: Request, res: Response) {
// TODO: Do something about the fradulent submission // TODO: Do something about the fradulent submission
Logger.warn("Caught a no-segment submission. userID: '" + userID + "', videoID: '" + videoID + "', category: '" + segments[i].category + "'"); Logger.warn("Caught a no-segment submission. userID: '" + userID + "', videoID: '" + videoID + "', category: '" + segments[i].category + "'");
res.status(403).send( res.status(403).send(
"Request rejected by auto moderator: New submissions are not allowed for the following category: '" "New submissions are not allowed for the following category: '"
+ segments[i].category + "'. A moderator has decided that no new segments are needed and that all current segments of this category are timed perfectly.\n\n " + segments[i].category + "'. A moderator has decided that no new segments are needed and that all current segments of this category are timed perfectly.\n\n "
+ (segments[i].category === "sponsor" ? "Maybe the segment you are submitting is a different category that you have not enabled and is not a sponsor. " + + (segments[i].category === "sponsor" ? "Maybe the segment you are submitting is a different category that you have not enabled and is not a sponsor. " +
"Categories that aren't sponsor, such as self-promotion can be enabled in the options.\n\n " : "") "Categories that aren't sponsor, such as self-promotion can be enabled in the options.\n\n" : "")
+ "If you believe this is incorrect, please contact someone on discord.gg/SponsorBlock or matrix.to/#/+sponsorblock:ajay.app", + "If you believe this is incorrect, please contact someone on discord.gg/SponsorBlock or matrix.to/#/+sponsorblock:ajay.app",
); );
return; return;
@@ -425,7 +426,9 @@ export async function postSkipSegments(req: Request, res: Response) {
let endTime = parseFloat(segments[i].segment[1]); let endTime = parseFloat(segments[i].segment[1]);
if (isNaN(startTime) || isNaN(endTime) if (isNaN(startTime) || isNaN(endTime)
|| startTime === Infinity || endTime === Infinity || startTime < 0 || startTime >= endTime) { || startTime === Infinity || endTime === Infinity || startTime < 0 || startTime > endTime
|| (getCategoryActionType(segments[i].category) === CategoryActionType.Skippable && startTime === endTime)
|| (getCategoryActionType(segments[i].category) === CategoryActionType.POI && startTime !== endTime)) {
//invalid request //invalid request
res.status(400).send("One of your segments times are invalid (too short, startTime before endTime, etc.)"); res.status(400).send("One of your segments times are invalid (too short, startTime before endTime, etc.)");
return; return;

View File

@@ -13,7 +13,8 @@ import {config} from '../config';
import { UserID } from '../types/user.model'; import { UserID } from '../types/user.model';
import redis from '../utils/redis'; import redis from '../utils/redis';
import { skipSegmentsHashKey, skipSegmentsKey } from '../middleware/redisKeys'; import { skipSegmentsHashKey, skipSegmentsKey } from '../middleware/redisKeys';
import { Category, HashedIP, IPAddress, SegmentUUID, Service, VideoID, VideoIDHash } from '../types/segments.model'; import { Category, CategoryActionType, HashedIP, IPAddress, SegmentUUID, Service, VideoID, VideoIDHash } from '../types/segments.model';
import { getCategoryActionType } from '../utils/categoryInfo';
const voteTypes = { const voteTypes = {
normal: 0, normal: 0,
@@ -59,9 +60,10 @@ async function sendWebhooks(voteData: VoteData) {
} }
if (config.youtubeAPIKey !== null) { if (config.youtubeAPIKey !== null) {
YouTubeAPI.listVideos(submissionInfoRow.videoID, (err, data) => { const { err, data } = await YouTubeAPI.listVideos(submissionInfoRow.videoID);
if (err || data.items.length === 0) { if (err || data.items.length === 0) {
err && Logger.error(err.toString()); if (err) Logger.error(err.toString());
return; return;
} }
const isUpvote = voteData.incrementAmount > 0; const isUpvote = voteData.incrementAmount > 0;
@@ -141,8 +143,6 @@ async function sendWebhooks(voteData: VoteData) {
Logger.error("\n"); Logger.error("\n");
}); });
} }
});
} }
} }
} }
@@ -170,6 +170,10 @@ async function categoryVote(UUID: SegmentUUID, userID: UserID, isVIP: boolean, i
res.status(400).send("Category doesn't exist."); res.status(400).send("Category doesn't exist.");
return; return;
} }
if (getCategoryActionType(category) !== CategoryActionType.Skippable) {
res.status(400).send("Cannot vote for this category");
return;
}
const nextCategoryInfo = await db.prepare("get", `select votes from "categoryVotes" where "UUID" = ? and category = ?`, [UUID, category]); const nextCategoryInfo = await db.prepare("get", `select votes from "categoryVotes" where "UUID" = ? and category = ?`, [UUID, category]);

View File

@@ -4,7 +4,7 @@ import { SBRecord } from "./lib.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 };
export type VideoDuration = number & { __videoDurationBrand: unknown }; export type VideoDuration = number & { __videoDurationBrand: unknown };
export type Category = string & { __categoryBrand: unknown }; export type Category = ("sponsor" | "selfpromo" | "interaction" | "intro" | "outro" | "preview" | "music_offtopic" | "highlight") & { __categoryBrand: unknown };
export type VideoIDHash = VideoID & HashedValue; export type VideoIDHash = VideoID & HashedValue;
export type IPAddress = string & { __ipAddressBrand: unknown }; export type IPAddress = string & { __ipAddressBrand: unknown };
export type HashedIP = IPAddress & HashedValue; export type HashedIP = IPAddress & HashedValue;
@@ -73,3 +73,8 @@ export interface SegmentCache {
shadowHiddenSegmentIPs: SBRecord<VideoID, {hashedIP: HashedIP}[]>, shadowHiddenSegmentIPs: SBRecord<VideoID, {hashedIP: HashedIP}[]>,
userHashedIP?: HashedIP userHashedIP?: HashedIP
} }
export enum CategoryActionType {
Skippable,
POI
}

10
src/utils/categoryInfo.ts Normal file
View File

@@ -0,0 +1,10 @@
import { Category, CategoryActionType } from "../types/segments.model";
export function getCategoryActionType(category: Category): CategoryActionType {
switch (category) {
case "highlight":
return CategoryActionType.POI;
default:
return CategoryActionType.Skippable;
}
}

View File

@@ -1,6 +1,7 @@
import {config} from '../config'; import {config} from '../config';
import {Logger} from '../utils/logger'; import {Logger} from '../utils/logger';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import AbortController from "abort-controller";
function getVoteAuthorRaw(submissionCount: number, isVIP: boolean, isOwnSubmission: boolean): string { function getVoteAuthorRaw(submissionCount: number, isVIP: boolean, isOwnSubmission: boolean): string {
if (isOwnSubmission) { if (isOwnSubmission) {
@@ -30,7 +31,8 @@ function dispatchEvent(scope: string, data: any): void {
let webhooks = config.webhooks; let webhooks = config.webhooks;
if (webhooks === undefined || webhooks.length === 0) return; if (webhooks === undefined || webhooks.length === 0) return;
Logger.debug("Dispatching webhooks"); Logger.debug("Dispatching webhooks");
webhooks.forEach(webhook => {
for (const webhook of webhooks) {
let webhookURL = webhook.url; let webhookURL = webhook.url;
let authKey = webhook.key; let authKey = webhook.key;
let scopes = webhook.scopes || []; let scopes = webhook.scopes || [];
@@ -43,13 +45,13 @@ function dispatchEvent(scope: string, data: any): void {
"Authorization": authKey, "Authorization": authKey,
"Event-Type": scope, // Maybe change this in the future? "Event-Type": scope, // Maybe change this in the future?
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, }
}) })
.catch(err => { .catch(err => {
Logger.warn('Couldn\'t send webhook to ' + webhook.url); Logger.warn('Couldn\'t send webhook to ' + webhook.url);
Logger.warn(err); Logger.warn(err);
}); });
}); }
} }
export { export {

View File

@@ -10,43 +10,45 @@ _youTubeAPI.authenticate({
}); });
export class YouTubeAPI { export class YouTubeAPI {
static listVideos(videoID: string, callback: (err: string | boolean, data: any) => void) { static async listVideos(videoID: string, ignoreCache = false): Promise<{err: string | boolean, data?: any}> {
const part = 'contentDetails,snippet'; const part = 'contentDetails,snippet';
if (!videoID || videoID.length !== 11 || videoID.includes(".")) { if (!videoID || videoID.length !== 11 || videoID.includes(".")) {
callback("Invalid video ID", undefined); return { err: "Invalid video ID" };
return;
} }
const redisKey = "youtube.video." + videoID; const redisKey = "youtube.video." + videoID;
redis.get(redisKey, (getErr, result) => { if (!ignoreCache) {
if (getErr || !result) { const {err, reply} = await redis.getAsync(redisKey);
if (!err && reply) {
Logger.debug("redis: no cache for video information: " + videoID); Logger.debug("redis: no cache for video information: " + videoID);
_youTubeAPI.videos.list({
return { err: err?.message, data: JSON.parse(reply) }
}
}
const { ytErr, data } = await new Promise((resolve) => _youTubeAPI.videos.list({
part, part,
id: videoID, id: videoID,
}, (ytErr: boolean | string, { data }: any) => { }, (ytErr: boolean | string, { data }: any) => resolve({ytErr, data})));
if (!ytErr) { if (!ytErr) {
// Only set cache if data returned // Only set cache if data returned
if (data.items.length > 0) { if (data.items.length > 0) {
redis.set(redisKey, JSON.stringify(data), (setErr) => { const { err: setErr } = await redis.setAsync(redisKey, JSON.stringify(data));
if (setErr) { if (setErr) {
Logger.warn(setErr.message); Logger.warn(setErr.message);
} else { } else {
Logger.debug("redis: video information cache set for: " + videoID); Logger.debug("redis: video information cache set for: " + videoID);
} }
callback(false, data); // don't fail
}); return { err: false, data }; // don't fail
} else { } else {
callback(false, data); // don't fail return { err: false, data }; // don't fail
} }
} else { } else {
callback(ytErr, data); return { err: ytErr, data };
} }
});
} else {
Logger.debug("redis: fetched video information from cache: " + videoID);
callback(getErr?.message, JSON.parse(result));
} }
});
};
} }

View File

@@ -40,16 +40,9 @@
"vote.up", "vote.up",
"vote.down" "vote.down"
] ]
}, {
"url": "http://unresolvable.host:8081/FailedWebhook",
"key": "superSecretKey",
"scopes": [
"vote.up",
"vote.down"
]
} }
], ],
"categoryList": ["sponsor", "selfpromo", "interaction", "intro", "outro", "preview", "music_offtopic"], "categoryList": ["sponsor", "selfpromo", "interaction", "intro", "outro", "preview", "music_offtopic", "highlight"],
"maxNumberOfActiveWarnings": 3, "maxNumberOfActiveWarnings": 3,
"hoursAfterWarningExpires": 24, "hoursAfterWarningExpires": 24,
"rateLimit": { "rateLimit": {

View File

@@ -19,6 +19,9 @@ describe('getSegmentsByHash', () => {
await db.prepare("run", startOfQuery + "('getSegmentsByHash-noMatchHash', 40, 50, 2, 'getSegmentsByHash-noMatchHash', 'testman', 0, 50, 'sponsor', 'YouTube', 0, 0, 'fdaffnoMatchHash')"); // hash = fdaff4dee1043451faa7398324fb63d8618ebcd11bddfe0491c488db12c6c910 await db.prepare("run", startOfQuery + "('getSegmentsByHash-noMatchHash', 40, 50, 2, 'getSegmentsByHash-noMatchHash', 'testman', 0, 50, 'sponsor', 'YouTube', 0, 0, 'fdaffnoMatchHash')"); // hash = fdaff4dee1043451faa7398324fb63d8618ebcd11bddfe0491c488db12c6c910
await db.prepare("run", startOfQuery + "('getSegmentsByHash-1', 60, 70, 2, 'getSegmentsByHash-1', 'testman', 0, 50, 'sponsor', 'YouTube', 0, 0, '" + getHash('getSegmentsByHash-1', 1) + "')"); // hash = 3272fa85ee0927f6073ef6f07ad5f3146047c1abba794cfa364d65ab9921692b await db.prepare("run", startOfQuery + "('getSegmentsByHash-1', 60, 70, 2, 'getSegmentsByHash-1', 'testman', 0, 50, 'sponsor', 'YouTube', 0, 0, '" + getHash('getSegmentsByHash-1', 1) + "')"); // hash = 3272fa85ee0927f6073ef6f07ad5f3146047c1abba794cfa364d65ab9921692b
await db.prepare("run", startOfQuery + "('onlyHidden', 60, 70, 2, 'onlyHidden', 'testman', 0, 50, 'sponsor', 'YouTube', 1, 0, '" + getHash('onlyHidden', 1) + "')"); // hash = f3a199e1af001d716cdc6599360e2b062c2d2b3fa2885f6d9d2fd741166cbbd3 await db.prepare("run", startOfQuery + "('onlyHidden', 60, 70, 2, 'onlyHidden', 'testman', 0, 50, 'sponsor', 'YouTube', 1, 0, '" + getHash('onlyHidden', 1) + "')"); // hash = f3a199e1af001d716cdc6599360e2b062c2d2b3fa2885f6d9d2fd741166cbbd3
await db.prepare("run", startOfQuery + "('highlightVid', 60, 60, 2, 'highlightVid-1', 'testman', 0, 50, 'highlight', 'YouTube', 0, 0, '" + getHash('highlightVid', 1) + "')"); // hash = c962d387a9e50170c9118405d20b1081cee8659cd600b856b511f695b91455cb
await db.prepare("run", startOfQuery + "('highlightVid', 70, 70, 2, 'highlightVid-2', 'testman', 0, 50, 'highlight', 'YouTube', 0, 0, '" + getHash('highlightVid', 1) + "')"); // hash = c962d387a9e50170c9118405d20b1081cee8659cd600b856b511f695b91455cb
}); });
it('Should be able to get a 200', (done: Done) => { it('Should be able to get a 200', (done: Done) => {
@@ -158,7 +161,7 @@ describe('getSegmentsByHash', () => {
if (res.status !== 200) done("non 200 status code, was " + res.status); if (res.status !== 200) done("non 200 status code, was " + res.status);
else { else {
const body = await res.json(); const body = await res.json();
if (body.length !== 1) done("expected 2 videos, got " + body.length); if (body.length !== 1) done("expected 1 video, got " + body.length);
else if (body[0].segments.length !== 1) done("expected 1 segments for first video, got " + body[0].segments.length); else if (body[0].segments.length !== 1) done("expected 1 segments for first video, got " + body[0].segments.length);
else if (body[0].segments[0].UUID !== 'getSegmentsByHash-0-0-1') done("both segments are not sponsor"); else if (body[0].segments[0].UUID !== 'getSegmentsByHash-0-0-1') done("both segments are not sponsor");
else done(); else done();
@@ -167,6 +170,20 @@ describe('getSegmentsByHash', () => {
.catch(err => done("Couldn't call endpoint")); .catch(err => done("Couldn't call endpoint"));
}); });
it('Should only return one segment when fetching highlight segments', (done: Done) => {
fetch(getbaseURL() + '/api/skipSegments/c962?category=highlight')
.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 !== 1) done("expected 1 segment, got " + body[0].segments.length);
else done();
}
})
.catch(err => done("Couldn't call endpoint"));
});
it('Should be able to post a segment and get it using endpoint', (done: Done) => { it('Should be able to post a segment and get it using endpoint', (done: Done) => {
let testID = 'abc123goodVideo'; let testID = 'abc123goodVideo';
fetch(getbaseURL() + "/api/postVideoSponsorTimes", { fetch(getbaseURL() + "/api/postVideoSponsorTimes", {

View File

@@ -500,6 +500,51 @@ describe('postSkipSegments', () => {
.catch(err => done("Couldn't call endpoint")); .catch(err => done("Couldn't call endpoint"));
}); });
it('Should be rejected if segment starts and ends at the same time', (done: Done) => {
fetch(getbaseURL()
+ "/api/skipSegments?videoID=qqwerty&startTime=90&endTime=90&userID=testing&category=intro", {
method: 'POST',
})
.then(async res => {
if (res.status === 400) done(); // pass
else {
const body = await res.text();
done("non 400 status code: " + res.status + " (" + body + ")");
}
})
.catch(err => done("Couldn't call endpoint"));
});
it('Should be accepted if highlight segment starts and ends at the same time', (done: Done) => {
fetch(getbaseURL()
+ "/api/skipSegments?videoID=qqwerty&startTime=30&endTime=30&userID=testing&category=highlight", {
method: 'POST',
})
.then(async res => {
if (res.status === 200) done(); // pass
else {
const body = await res.text();
done("non 200 status code: " + res.status + " (" + body + ")");
}
})
.catch(err => done("Couldn't call endpoint"));
});
it('Should be rejected if highlight segment doesn\'t start and end at the same time', (done: Done) => {
fetch(getbaseURL()
+ "/api/skipSegments?videoID=qqwerty&startTime=30&endTime=30.5&userID=testing&category=highlight", {
method: 'POST',
})
.then(async res => {
if (res.status === 400) done(); // pass
else {
const body = await res.text();
done("non 400 status code: " + res.status + " (" + body + ")");
}
})
.catch(err => done("Couldn't call endpoint"));
});
it('Should be rejected if a sponsor is less than 1 second', (done: Done) => { it('Should be rejected if a sponsor is less than 1 second', (done: Done) => {
fetch(getbaseURL() fetch(getbaseURL()
+ "/api/skipSegments?videoID=qqwerty&startTime=30&endTime=30.5&userID=testing", { + "/api/skipSegments?videoID=qqwerty&startTime=30&endTime=30.5&userID=testing", {

View File

@@ -264,6 +264,24 @@ describe('voteOnSponsorTime', () => {
.catch(err => done(err)); .catch(err => done(err));
}); });
it('Should not able to change to highlight category', (done: Done) => {
fetch(getbaseURL()
+ "/api/voteOnSponsorTime?userID=randomID2&UUID=incorrect-category&category=highlight")
.then(async res => {
if (res.status === 400) {
let row = await db.prepare('get', `SELECT "category" FROM "sponsorTimes" WHERE "UUID" = ?`, ["incorrect-category"]);
if (row.category === "sponsor") {
done();
} else {
done("Vote did not succeed. Submission went from sponsor to " + row.category);
}
} else {
done("Status code was " + res.status);
}
})
.catch(err => done(err));
});
it('Should be able to change your vote for a category and it should add your vote to the database', (done: Done) => { it('Should be able to change your vote for a category and it should add your vote to the database', (done: Done) => {
fetch(getbaseURL() fetch(getbaseURL()
+ "/api/voteOnSponsorTime?userID=randomID2&UUID=vote-uuid-4&category=outro") + "/api/voteOnSponsorTime?userID=randomID2&UUID=vote-uuid-4&category=outro")

View File

@@ -9,21 +9,27 @@ YouTubeAPI.videos.list({
export class YouTubeApiMock { export class YouTubeApiMock {
static listVideos(videoID: string, callback: (ytErr: any, data: any) => void) { static async listVideos(videoID: string, ignoreCache = false): Promise<{err: string | boolean, data?: any}> {
const obj = { const obj = {
id: videoID id: videoID
}; };
if (obj.id === "knownWrongID") { if (obj.id === "knownWrongID") {
callback(undefined, { return {
err: null,
data: {
pageInfo: { pageInfo: {
totalResults: 0, totalResults: 0,
}, },
items: [], items: [],
});
} }
};
}
if (obj.id === "noDuration") { if (obj.id === "noDuration") {
callback(undefined, { return {
err: null,
data: {
pageInfo: { pageInfo: {
totalResults: 1, totalResults: 1,
}, },
@@ -42,9 +48,12 @@ export class YouTubeApiMock {
}, },
}, },
], ],
}); }
};
} else { } else {
callback(undefined, { return {
err: null,
data: {
pageInfo: { pageInfo: {
totalResults: 1, totalResults: 1,
}, },
@@ -63,7 +72,8 @@ export class YouTubeApiMock {
}, },
}, },
], ],
}); }
};
} }
} }
} }