Merge pull request #448 from ajayyy/poi-highlight

Highlight category now has it's own action type
This commit is contained in:
Ajay Ramachandran
2022-01-19 19:34:35 -05:00
committed by GitHub
8 changed files with 71 additions and 32 deletions

View File

@@ -0,0 +1,7 @@
BEGIN TRANSACTION;
UPDATE "sponsorTimes" SET "actionType" = 'poi' WHERE "category" = 'poi_highlight';
UPDATE "config" SET value = 30 WHERE key = 'version';
COMMIT;

View File

@@ -30,7 +30,7 @@ addDefaults(config, {
preview: ["skip", "mute"], preview: ["skip", "mute"],
filler: ["skip", "mute"], filler: ["skip", "mute"],
music_offtopic: ["skip"], music_offtopic: ["skip"],
poi_highlight: ["skip"], poi_highlight: ["poi"],
chapter: ["chapter"] chapter: ["chapter"]
}, },
maxNumberOfActiveWarnings: 1, maxNumberOfActiveWarnings: 1,

View File

@@ -4,8 +4,7 @@ import { config } from "../config";
import { db, privateDB } from "../databases/databases"; import { db, privateDB } from "../databases/databases";
import { skipSegmentsHashKey, skipSegmentsKey, skipSegmentGroupsKey } from "../utils/redisKeys"; import { skipSegmentsHashKey, skipSegmentsKey, skipSegmentGroupsKey } from "../utils/redisKeys";
import { SBRecord } from "../types/lib.model"; import { SBRecord } from "../types/lib.model";
import { ActionType, Category, CategoryActionType, DBSegment, HashedIP, IPAddress, OverlappingSegmentGroup, Segment, SegmentCache, SegmentUUID, Service, VideoData, VideoID, VideoIDHash, Visibility, VotableObject } from "../types/segments.model"; import { ActionType, Category, DBSegment, HashedIP, IPAddress, OverlappingSegmentGroup, Segment, SegmentCache, SegmentUUID, Service, VideoData, VideoID, VideoIDHash, Visibility, VotableObject } from "../types/segments.model";
import { getCategoryActionType } from "../utils/categoryInfo";
import { getHashCache } from "../utils/getHashCache"; import { getHashCache } from "../utils/getHashCache";
import { getIP } from "../utils/getIP"; import { getIP } from "../utils/getIP";
import { Logger } from "../utils/logger"; import { Logger } from "../utils/logger";
@@ -66,6 +65,12 @@ async function getSegmentsByVideoID(req: Request, videoID: VideoID, categories:
actionTypes: ActionType[], requiredSegments: SegmentUUID[], service: Service): Promise<Segment[]> { actionTypes: ActionType[], requiredSegments: SegmentUUID[], service: Service): Promise<Segment[]> {
const cache: SegmentCache = { shadowHiddenSegmentIPs: {} }; const cache: SegmentCache = { shadowHiddenSegmentIPs: {} };
// For old clients
const forcePoiAsSkip = !actionTypes.includes(ActionType.Poi) && categories.includes("poi_highlight" as Category);
if (forcePoiAsSkip) {
actionTypes.push(ActionType.Poi);
}
try { try {
categories = categories.filter((category) => !/[^a-z|_|-]/.test(category)); categories = categories.filter((category) => !/[^a-z|_|-]/.test(category));
if (categories.length === 0) return null; if (categories.length === 0) return null;
@@ -77,9 +82,17 @@ async function getSegmentsByVideoID(req: Request, videoID: VideoID, categories:
}, {}); }, {});
const canUseCache = requiredSegments.length === 0; const canUseCache = requiredSegments.length === 0;
const processedSegments: Segment[] = await prepareCategorySegments(req, videoID, service, segments, cache, canUseCache); let processedSegments: Segment[] = (await prepareCategorySegments(req, videoID, service, segments, cache, canUseCache))
.filter((segment: Segment) => categories.includes(segment?.category) && (actionTypes.includes(segment?.actionType)));
return processedSegments.filter((segment: Segment) => categories.includes(segment?.category) && actionTypes.includes(segment?.actionType)); if (forcePoiAsSkip) {
processedSegments = processedSegments.map((segment) => ({
...segment,
actionType: ActionType.Skip
}));
}
return processedSegments;
} catch (err) { } catch (err) {
if (err) { if (err) {
Logger.error(err as string); Logger.error(err as string);
@@ -93,6 +106,12 @@ async function getSegmentsByHash(req: Request, hashedVideoIDPrefix: VideoIDHash,
const cache: SegmentCache = { shadowHiddenSegmentIPs: {} }; const cache: SegmentCache = { shadowHiddenSegmentIPs: {} };
const segments: SBRecord<VideoID, VideoData> = {}; const segments: SBRecord<VideoID, VideoData> = {};
// For old clients
const forcePoiAsSkip = !actionTypes.includes(ActionType.Poi) && categories.includes("poi_highlight" as Category);
if (forcePoiAsSkip) {
actionTypes.push(ActionType.Poi);
}
try { try {
type SegmentWithHashPerVideoID = SBRecord<VideoID, { hash: VideoIDHash, segments: DBSegment[] }>; type SegmentWithHashPerVideoID = SBRecord<VideoID, { hash: VideoIDHash, segments: DBSegment[] }>;
@@ -123,6 +142,13 @@ async function getSegmentsByHash(req: Request, hashedVideoIDPrefix: VideoIDHash,
data.segments = (await prepareCategorySegments(req, videoID as VideoID, service, videoData.segments, cache, canUseCache)) data.segments = (await prepareCategorySegments(req, videoID as VideoID, service, videoData.segments, cache, canUseCache))
.filter((segment: Segment) => categories.includes(segment?.category) && actionTypes.includes(segment?.actionType)); .filter((segment: Segment) => categories.includes(segment?.category) && actionTypes.includes(segment?.actionType));
if (forcePoiAsSkip) {
data.segments = data.segments.map((segment) => ({
...segment,
actionType: ActionType.Skip
}));
}
if (data.segments.length > 0) { if (data.segments.length > 0) {
segments[videoID] = data; segments[videoID] = data;
} }
@@ -236,7 +262,7 @@ async function chooseSegments(videoID: VideoID, service: Service, segments: DBSe
// Filter for only 1 item for POI categories and Full video // Filter for only 1 item for POI categories and Full video
let chosenGroups = getWeightedRandomChoice(groups, 1, true, (choice) => choice.segments[0].actionType === ActionType.Full); let chosenGroups = getWeightedRandomChoice(groups, 1, true, (choice) => choice.segments[0].actionType === ActionType.Full);
chosenGroups = getWeightedRandomChoice(chosenGroups, 1, true, (choice) => getCategoryActionType(choice.segments[0].category) === CategoryActionType.POI); chosenGroups = getWeightedRandomChoice(chosenGroups, 1, true, (choice) => choice.segments[0].actionType === ActionType.Poi);
return chosenGroups.map(//randomly choose 1 good segment per group and return them return chosenGroups.map(//randomly choose 1 good segment per group and return them
group => getWeightedRandomChoice(group.segments, 1)[0] group => getWeightedRandomChoice(group.segments, 1)[0]
); );

View File

@@ -9,9 +9,8 @@ import { getIP } from "../utils/getIP";
import { getFormattedTime } from "../utils/getFormattedTime"; import { getFormattedTime } from "../utils/getFormattedTime";
import { dispatchEvent } from "../utils/webhookUtils"; import { dispatchEvent } from "../utils/webhookUtils";
import { Request, Response } from "express"; import { Request, Response } from "express";
import { ActionType, Category, CategoryActionType, IncomingSegment, IPAddress, SegmentUUID, Service, VideoDuration, VideoID } from "../types/segments.model"; import { ActionType, Category, IncomingSegment, IPAddress, SegmentUUID, Service, VideoDuration, VideoID } from "../types/segments.model";
import { deleteLockCategories } from "./deleteLockCategories"; import { deleteLockCategories } from "./deleteLockCategories";
import { getCategoryActionType } from "../utils/categoryInfo";
import { QueryCacher } from "../utils/queryCacher"; import { QueryCacher } from "../utils/queryCacher";
import { getReputation } from "../utils/reputation"; import { getReputation } from "../utils/reputation";
import { APIVideoData, APIVideoInfo } from "../types/youtubeApi.model"; import { APIVideoData, APIVideoInfo } from "../types/youtubeApi.model";
@@ -375,6 +374,11 @@ async function checkEachSegmentValid(rawIP: IPAddress, paramUserID: UserID, user
}; };
} }
// For old clients
if (segments[i].category === "poi_highlight" && segments[i].actionType !== ActionType.Poi) {
segments[i].actionType = ActionType.Poi;
}
if (!config.categorySupport[segments[i].category]?.includes(segments[i].actionType)) { if (!config.categorySupport[segments[i].category]?.includes(segments[i].actionType)) {
return { pass: false, errorMessage: "ActionType is not supported with this category.", errorCode: 400 }; return { pass: false, errorMessage: "ActionType is not supported with this category.", errorCode: 400 };
} }
@@ -384,16 +388,16 @@ async function checkEachSegmentValid(rawIP: IPAddress, paramUserID: UserID, user
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 || (segments[i].actionType !== ActionType.Poi
&& segments[i].actionType !== ActionType.Full && startTime === endTime) && segments[i].actionType !== ActionType.Full && startTime === endTime)
|| (getCategoryActionType(segments[i].category) === CategoryActionType.POI && startTime !== endTime) || (segments[i].actionType === ActionType.Poi && startTime !== endTime)
|| (segments[i].actionType === ActionType.Full && (startTime !== 0 || endTime !== 0))) { || (segments[i].actionType === ActionType.Full && (startTime !== 0 || endTime !== 0))) {
//invalid request //invalid request
return { pass: false, errorMessage: "One of your segments times are invalid (too short, endTime before startTime, etc.)", errorCode: 400 }; return { pass: false, errorMessage: "One of your segments times are invalid (too short, endTime before startTime, etc.)", errorCode: 400 };
} }
// Check for POI segments before some seconds // Check for POI segments before some seconds
if (!isVIP && getCategoryActionType(segments[i].category) === CategoryActionType.POI && startTime < config.poiMinimumStartTime) { if (!isVIP && segments[i].actionType === ActionType.Poi && startTime < config.poiMinimumStartTime) {
return { pass: false, errorMessage: `POI cannot be that early`, errorCode: 400 }; return { pass: false, errorMessage: `POI cannot be that early`, errorCode: 400 };
} }

View File

@@ -10,8 +10,7 @@ import { getIP } from "../utils/getIP";
import { getHashCache } from "../utils/getHashCache"; import { getHashCache } from "../utils/getHashCache";
import { config } from "../config"; import { config } from "../config";
import { HashedUserID, UserID } from "../types/user.model"; import { HashedUserID, UserID } from "../types/user.model";
import { Category, CategoryActionType, HashedIP, IPAddress, SegmentUUID, Service, VideoID, VideoIDHash, Visibility, VideoDuration, ActionType } from "../types/segments.model"; import { Category, HashedIP, IPAddress, SegmentUUID, Service, VideoID, VideoIDHash, Visibility, VideoDuration, ActionType } from "../types/segments.model";
import { getCategoryActionType } from "../utils/categoryInfo";
import { QueryCacher } from "../utils/queryCacher"; import { QueryCacher } from "../utils/queryCacher";
import axios from "axios"; import axios from "axios";
import redis from "../utils/redis"; import redis from "../utils/redis";
@@ -211,8 +210,8 @@ async function categoryVote(UUID: SegmentUUID, userID: UserID, isVIP: boolean, i
if (!config.categoryList.includes(category)) { if (!config.categoryList.includes(category)) {
return { status: 400, message: "Category doesn't exist." }; return { status: 400, message: "Category doesn't exist." };
} }
if (getCategoryActionType(category) !== CategoryActionType.Skippable) { if (videoInfo.actionType === ActionType.Poi) {
return { status: 400, message: "Cannot vote for this category" }; return { status: 400, message: "Not allowed to change category for single point segments" };
} }
// Ignore vote if the next category is locked // Ignore vote if the next category is locked

View File

@@ -14,7 +14,8 @@ export enum ActionType {
Skip = "skip", Skip = "skip",
Mute = "mute", Mute = "mute",
Chapter = "chapter", Chapter = "chapter",
Full = "full" Full = "full",
Poi = "poi"
} }
// Uncomment as needed // Uncomment as needed
@@ -102,11 +103,6 @@ export interface SegmentCache {
userHashedIP?: HashedIP userHashedIP?: HashedIP
} }
export enum CategoryActionType {
Skippable,
POI
}
export interface DBLock { export interface DBLock {
videoID: VideoID, videoID: VideoID,
userID: HashedUserID, userID: HashedUserID,

View File

@@ -1,9 +0,0 @@
import { Category, CategoryActionType } from "../types/segments.model";
export function getCategoryActionType(category: Category): CategoryActionType {
if (category.startsWith("poi_")) {
return CategoryActionType.POI;
} else {
return CategoryActionType.Skippable;
}
}

View File

@@ -28,8 +28,8 @@ describe("getSkipSegmentsByHash", () => {
await db.prepare("run", query, ["getSegmentsByHash-noMatchHash", 40, 50, 2, 0, "getSegmentsByHash-noMatchHash", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, "fdaffnoMatchHash", ""]); await db.prepare("run", query, ["getSegmentsByHash-noMatchHash", 40, 50, 2, 0, "getSegmentsByHash-noMatchHash", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, "fdaffnoMatchHash", ""]);
await db.prepare("run", query, ["getSegmentsByHash-1", 60, 70, 2, 0, "getSegmentsByHash-1", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, "3272fa85ee0927f6073ef6f07ad5f3146047c1abba794cfa364d65ab9921692b", ""]); await db.prepare("run", query, ["getSegmentsByHash-1", 60, 70, 2, 0, "getSegmentsByHash-1", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, "3272fa85ee0927f6073ef6f07ad5f3146047c1abba794cfa364d65ab9921692b", ""]);
await db.prepare("run", query, ["onlyHidden", 60, 70, 2, 0, "onlyHidden", "testman", 0, 50, "sponsor", "skip", "YouTube", 1, 0, "f3a199e1af001d716cdc6599360e2b062c2d2b3fa2885f6d9d2fd741166cbbd3", ""]); await db.prepare("run", query, ["onlyHidden", 60, 70, 2, 0, "onlyHidden", "testman", 0, 50, "sponsor", "skip", "YouTube", 1, 0, "f3a199e1af001d716cdc6599360e2b062c2d2b3fa2885f6d9d2fd741166cbbd3", ""]);
await db.prepare("run", query, ["highlightVid", 60, 60, 2, 0, "highlightVid-1", "testman", 0, 50, "poi_highlight", "skip", "YouTube", 0, 0, getHash("highlightVid", 1), ""]); await db.prepare("run", query, ["highlightVid", 60, 60, 2, 0, "highlightVid-1", "testman", 0, 50, "poi_highlight", "poi", "YouTube", 0, 0, getHash("highlightVid", 1), ""]);
await db.prepare("run", query, ["highlightVid", 70, 70, 2, 0, "highlightVid-2", "testman", 0, 50, "poi_highlight", "skip", "YouTube", 0, 0, getHash("highlightVid", 1), ""]); await db.prepare("run", query, ["highlightVid", 70, 70, 2, 0, "highlightVid-2", "testman", 0, 50, "poi_highlight", "poi", "YouTube", 0, 0, getHash("highlightVid", 1), ""]);
await db.prepare("run", query, ["requiredSegmentVid", 60, 70, 2, 0, "requiredSegmentVid-1", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, requiredSegmentVidHash, ""]); await db.prepare("run", query, ["requiredSegmentVid", 60, 70, 2, 0, "requiredSegmentVid-1", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, requiredSegmentVidHash, ""]);
await db.prepare("run", query, ["requiredSegmentVid", 60, 70, -2, 0, "requiredSegmentVid-2", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, requiredSegmentVidHash, ""]); await db.prepare("run", query, ["requiredSegmentVid", 60, 70, -2, 0, "requiredSegmentVid-2", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, requiredSegmentVidHash, ""]);
await db.prepare("run", query, ["requiredSegmentVid", 80, 90, -2, 0, "requiredSegmentVid-3", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, requiredSegmentVidHash, ""]); await db.prepare("run", query, ["requiredSegmentVid", 80, 90, -2, 0, "requiredSegmentVid-3", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, requiredSegmentVidHash, ""]);
@@ -276,12 +276,28 @@ describe("getSkipSegmentsByHash", () => {
}); });
it("Should only return one segment when fetching highlight segments", (done) => { it("Should only return one segment when fetching highlight segments", (done) => {
client.get(`${endpoint}/c962`, { params: { category: "poi_highlight", actionType: "poi" } })
.then(res => {
assert.strictEqual(res.status, 200);
const data = res.data;
assert.strictEqual(data.length, 1);
assert.strictEqual(data[0].segments.length, 1);
assert.strictEqual(data[0].segments[0].category, "poi_highlight");
assert.strictEqual(data[0].segments[0].actionType, "poi");
done();
})
.catch(err => done(err));
});
it("Should return skip actionType for highlight for old clients", (done) => {
client.get(`${endpoint}/c962`, { params: { category: "poi_highlight" } }) client.get(`${endpoint}/c962`, { params: { category: "poi_highlight" } })
.then(res => { .then(res => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const data = res.data; const data = res.data;
assert.strictEqual(data.length, 1); assert.strictEqual(data.length, 1);
assert.strictEqual(data[0].segments.length, 1); assert.strictEqual(data[0].segments.length, 1);
assert.strictEqual(data[0].segments[0].category, "poi_highlight");
assert.strictEqual(data[0].segments[0].actionType, "skip");
done(); done();
}) })
.catch(err => done(err)); .catch(err => done(err));