diff --git a/api-prop.md b/api-prop.md new file mode 100644 index 0000000..e8cd3d6 --- /dev/null +++ b/api-prop.md @@ -0,0 +1,48 @@ +GET /api/searchSegments +Input: (URL Params, translated 1-1 to JSON) +```ts +{ + // same as skipSegments + videoID: string, + + category: string + categories: string[] + + actionType: string + actionTypes: string[] + + service: string + // end skipSegments + + page: int // page to start from (default 0) + + minVotes: int // default -2, vote threshold, inclusive + maxVotes: int // default infinite, vote threshold, inclusive + + minViews: int // default 0 - view threshold, inclusive + maxViews: int // default infinite - view threshold, inclusive + + locked: boolean // default true - if false, dont't show segments that are locked + hidden: boolean // default true - if false, don't show segment that are hidden/ shadowhidden + ignored: boolean // default true - if false, don't show segments that are hidden or below vote threshold +} +``` + +Response: (JSON) +```ts + segmentCount: int, // total number of segements matching query + page: int, // page number + segments: [{ // array of this object, max 10 + UUID: string, + timeSubmitted: int, // time submitted + startTime: int, // start time in seconds + endTime: int, // end time in seconds + category: string, // category of segment + actionType: string, // action type of segment + votes: int, // number of votes + views: int // number of views + locked: int, // locked + hidden: int, // hidden + shadowHidden: int, // shadowHidden + }] +``` \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index 5e97f05..706d53b 100644 --- a/src/app.ts +++ b/src/app.ts @@ -35,6 +35,7 @@ import {postPurgeAllSegments} from "./routes/postPurgeAllSegments"; import {getUserID} from "./routes/getUserID"; import {getLockCategories} from "./routes/getLockCategories"; import {getLockCategoriesByHash} from "./routes/getLockCategoriesByHash"; +import {endpoint as getSearchSegments } from "./routes/getSearchSegments"; import ExpressPromiseRouter from "express-promise-router"; import { Server } from "http"; @@ -164,6 +165,9 @@ function setupRoutes(router: Router) { // get privacy protecting lock categories functions router.get("/api/lockCategories/:prefix", getLockCategoriesByHash); + // get all segments that match a search + router.get("/api/searchSegments", getSearchSegments); + if (config.postgres) { router.get("/database", (req, res) => dumpDatabase(req, res, true)); router.get("/database.json", (req, res) => dumpDatabase(req, res, false)); diff --git a/src/routes/getSearchSegments.ts b/src/routes/getSearchSegments.ts new file mode 100644 index 0000000..a73f9ca --- /dev/null +++ b/src/routes/getSearchSegments.ts @@ -0,0 +1,143 @@ +import { Request, Response } from "express"; +import { db } from "../databases/databases"; +import { ActionType, Category, DBSegment, Service, VideoID } from "../types/segments.model"; +const segmentsPerPage = 10; + +type searchSegmentResponse = { + segmentCount: number, + page: number, + segments: DBSegment[] +}; + +async function getSegmentsFromDBByVideoID(videoID: VideoID, service: Service): Promise { + return db.prepare( + "all", + `SELECT "UUID", "timeSubmitted", "startTime", "endTime", "category", "actionType", "votes", "views", "locked", "hidden", "shadowHidden", FROM "sponsorTimes" + WHERE "videoID" = ? AND "service" = ? ORDER BY "UUID"`, + [videoID, service] + ) as Promise; +} + +/** + * + * Returns what would be sent to the client. + * Will respond with errors if required. Returns false if it errors. + * + * @param req + * @param res + * + * @returns + */ +async function handleGetSegments(req: Request, res: Response): Promise { + const videoID = req.query.videoID as VideoID; + if (!videoID) { + res.status(400).send("videoID not specified"); + return false; + } + // Default to sponsor + // If using params instead of JSON, only one category can be pulled + const categories: Category[] = req.query.categories + ? JSON.parse(req.query.categories as string) + : req.query.category + ? Array.isArray(req.query.category) + ? req.query.category + : [req.query.category] + : ["sponsor"]; + if (!Array.isArray(categories)) { + res.status(400).send("Categories parameter does not match format requirements."); + 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; + } + + let service: Service = req.query.service ?? req.body.service ?? Service.YouTube; + if (!Object.values(Service).some((val) => val == service)) { + service = Service.YouTube; + } + + const page: number = req.query.page ?? req.body.page ?? 0; + + const minVotes: number = req.query.minVotes ?? req.body.minVotes ?? -2; + const maxVotes: number = req.query.maxVotes ?? req.body.maxVotes ?? Infinity; + + const minViews: number = req.query.minViews ?? req.body.minViews ?? 0; + const maxViews: number = req.query.maxViews ?? req.body.maxViews ?? Infinity; + + const locked: boolean = req.query.locked ?? req.body.locked ?? true; + const hidden: boolean = req.query.hidden ?? req.body.hidden ?? true; + const ignored: boolean = req.query.ignored ?? req.body.ignored ?? true; + + const filters = { + minVotes, + maxVotes, + minViews, + maxViews, + locked, + hidden, + ignored + }; + + const segments = await getSegmentsFromDBByVideoID(videoID, service); + + if (segments === null || segments === undefined) { + res.sendStatus(500); + return false; + } + + if (segments.length === 0) { + res.sendStatus(404); + return false; + } + + return filterSegments(segments, page, filters); +} + +function filterSegments(segments: DBSegment[], page: number, filters: Record) { + const startIndex = 0+(page*segmentsPerPage); + const endIndex = segmentsPerPage+(page*segmentsPerPage); + const filteredSegments = segments.filter((segment) => + (!(segment.votes <= filters.minVotes || segment.votes >= filters.maxVotes) + || (segment.views <= filters.minViews || segment.views >= filters.maxViews) + || (filters.locked && segment.locked) + || (filters.hidden && segment.hidden) + || (filters.ignored && (segment.hidden || segment.shadowHidden)) + ) + // return false if any of the conditions are met + // return true if none of the conditions are met + ); + return { + segmentCount: filteredSegments.length, + page, + segments: filteredSegments.slice(startIndex, endIndex) + }; +} + + +async function endpoint(req: Request, res: Response): Promise { + try { + const segmentResponse = await handleGetSegments(req, res); + // If false, res.send has already been called + if (segmentResponse) { + //send result + return res.send(segmentResponse); + } + } catch (err) { + if (err instanceof SyntaxError) { + return res.status(400).send("Invalid array in parameters"); + } else return res.sendStatus(500); + } +} + +export { + endpoint +}; diff --git a/src/types/segments.model.ts b/src/types/segments.model.ts index be4745f..b863a15 100644 --- a/src/types/segments.model.ts +++ b/src/types/segments.model.ts @@ -53,7 +53,9 @@ export interface DBSegment { UUID: SegmentUUID; userID: UserID; votes: number; + views: number; locked: boolean; + hidden: boolean; required: boolean; // Requested specifically from the client shadowHidden: Visibility; videoID: VideoID;