mirror of
https://github.com/ajayyy/SponsorBlockServer.git
synced 2025-12-07 12:07:07 +03:00
add getSearchSegments endpoint
This commit is contained in:
48
api-prop.md
Normal file
48
api-prop.md
Normal file
@@ -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
|
||||||
|
}]
|
||||||
|
```
|
||||||
@@ -35,6 +35,7 @@ import {postPurgeAllSegments} from "./routes/postPurgeAllSegments";
|
|||||||
import {getUserID} from "./routes/getUserID";
|
import {getUserID} from "./routes/getUserID";
|
||||||
import {getLockCategories} from "./routes/getLockCategories";
|
import {getLockCategories} from "./routes/getLockCategories";
|
||||||
import {getLockCategoriesByHash} from "./routes/getLockCategoriesByHash";
|
import {getLockCategoriesByHash} from "./routes/getLockCategoriesByHash";
|
||||||
|
import {endpoint as getSearchSegments } from "./routes/getSearchSegments";
|
||||||
import ExpressPromiseRouter from "express-promise-router";
|
import ExpressPromiseRouter from "express-promise-router";
|
||||||
import { Server } from "http";
|
import { Server } from "http";
|
||||||
|
|
||||||
@@ -164,6 +165,9 @@ function setupRoutes(router: Router) {
|
|||||||
// get privacy protecting lock categories functions
|
// get privacy protecting lock categories functions
|
||||||
router.get("/api/lockCategories/:prefix", getLockCategoriesByHash);
|
router.get("/api/lockCategories/:prefix", getLockCategoriesByHash);
|
||||||
|
|
||||||
|
// get all segments that match a search
|
||||||
|
router.get("/api/searchSegments", getSearchSegments);
|
||||||
|
|
||||||
if (config.postgres) {
|
if (config.postgres) {
|
||||||
router.get("/database", (req, res) => dumpDatabase(req, res, true));
|
router.get("/database", (req, res) => dumpDatabase(req, res, true));
|
||||||
router.get("/database.json", (req, res) => dumpDatabase(req, res, false));
|
router.get("/database.json", (req, res) => dumpDatabase(req, res, false));
|
||||||
|
|||||||
143
src/routes/getSearchSegments.ts
Normal file
143
src/routes/getSearchSegments.ts
Normal file
@@ -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<DBSegment[]> {
|
||||||
|
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<DBSegment[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* 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<searchSegmentResponse | false> {
|
||||||
|
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<string, string|boolean|number>) {
|
||||||
|
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<Response> {
|
||||||
|
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
|
||||||
|
};
|
||||||
@@ -53,7 +53,9 @@ export interface DBSegment {
|
|||||||
UUID: SegmentUUID;
|
UUID: SegmentUUID;
|
||||||
userID: UserID;
|
userID: UserID;
|
||||||
votes: number;
|
votes: number;
|
||||||
|
views: number;
|
||||||
locked: boolean;
|
locked: boolean;
|
||||||
|
hidden: boolean;
|
||||||
required: boolean; // Requested specifically from the client
|
required: boolean; // Requested specifically from the client
|
||||||
shadowHidden: Visibility;
|
shadowHidden: Visibility;
|
||||||
videoID: VideoID;
|
videoID: VideoID;
|
||||||
|
|||||||
Reference in New Issue
Block a user