add innerTube as primary videoInfo endpoint

- drop videoInfo.genreUrl since it's always empty
- bump ts target to ES2021 for Promise.any
- fix mocks to return err: false
- get maxResThumbnail from static endpoint
This commit is contained in:
Michael C 2022-09-15 17:02:33 -04:00
parent 3c09033267
commit 62a9b0eddd
No known key found for this signature in database
GPG key ID: FFB04FB3B878B7B4
15 changed files with 130 additions and 109 deletions

View file

@ -126,7 +126,6 @@
| channelID | TEXT | not null |
| title | TEXT | not null |
| published | REAL | not null |
| genreUrl | TEXT | not null |
| index | field |
| -- | :--: |

View file

@ -0,0 +1,7 @@
BEGIN TRANSACTION;
ALTER TABLE "videoInfo" DROP COLUMN "genreUrl";
UPDATE "config" SET value = 34 WHERE key = 'version';
COMMIT;

View file

@ -1,7 +1,5 @@
import { VideoID } from "../types/segments.model";
import { YouTubeAPI } from "../utils/youtubeApi";
import { APIVideoInfo } from "../types/youtubeApi.model";
import { config } from "../config";
import { getVideoDetails } from "../utils/getVideoDetails";
import { getHashCache } from "../utils/getHashCache";
import { privateDB } from "../databases/databases";
import { Request, Response } from "express";
@ -20,15 +18,11 @@ interface AddUserAsTempVIPRequest extends Request {
}
}
function getYouTubeVideoInfo(videoID: VideoID, ignoreCache = false): Promise<APIVideoInfo> {
return (config.newLeafURLs) ? YouTubeAPI.listVideos(videoID, ignoreCache) : null;
}
const getChannelInfo = async (videoID: VideoID): Promise<{id: string | null, name: string | null }> => {
const videoInfo = await getYouTubeVideoInfo(videoID);
const videoInfo = await getVideoDetails(videoID);
return {
id: videoInfo?.data?.authorId,
name: videoInfo?.data?.author
id: videoInfo?.authorId,
name: videoInfo?.authorName
};
};

View file

@ -1,7 +1,7 @@
import { config } from "../config";
import { Logger } from "../utils/logger";
import { db, privateDB } from "../databases/databases";
import { getMaxResThumbnail, YouTubeAPI } from "../utils/youtubeApi";
import { getMaxResThumbnail } from "../utils/youtubeApi";
import { getSubmissionUUID } from "../utils/getSubmissionUUID";
import { getHash } from "../utils/getHash";
import { getHashCache } from "../utils/getHashCache";
@ -13,7 +13,6 @@ import { ActionType, Category, IncomingSegment, IPAddress, SegmentUUID, Service,
import { deleteLockCategories } from "./deleteLockCategories";
import { QueryCacher } from "../utils/queryCacher";
import { getReputation } from "../utils/reputation";
import { APIVideoData, APIVideoInfo } from "../types/youtubeApi.model";
import { HashedUserID, UserID } from "../types/user.model";
import { isUserVIP } from "../utils/isUserVIP";
import { isUserTempVIP } from "../utils/isUserTempVIP";
@ -22,6 +21,7 @@ import { getService } from "../utils/getService";
import axios from "axios";
import { vote } from "./voteOnSponsorTime";
import { canSubmit } from "../utils/permissions";
import { getVideoDetails, videoDetails } from "../utils/getVideoDetails";
type CheckResult = {
pass: boolean,
@ -35,7 +35,7 @@ const CHECK_PASS: CheckResult = {
errorCode: 0
};
async function sendWebhookNotification(userID: string, videoID: string, UUID: string, submissionCount: number, youtubeData: APIVideoData, { submissionStart, submissionEnd }: { submissionStart: number; submissionEnd: number; }, segmentInfo: any) {
async function sendWebhookNotification(userID: string, videoID: string, UUID: string, submissionCount: number, youtubeData: videoDetails, { submissionStart, submissionEnd }: { submissionStart: number; submissionEnd: number; }, segmentInfo: any) {
const row = await db.prepare("get", `SELECT "userName" FROM "userNames" WHERE "userID" = ?`, [userID]);
const userName = row !== undefined ? row.userName : null;
@ -48,7 +48,7 @@ async function sendWebhookNotification(userID: string, videoID: string, UUID: st
"video": {
"id": videoID,
"title": youtubeData?.title,
"thumbnail": getMaxResThumbnail(youtubeData) || null,
"thumbnail": getMaxResThumbnail(videoID),
"url": `https://www.youtube.com/watch?v=${videoID}`,
},
"submission": {
@ -64,16 +64,13 @@ async function sendWebhookNotification(userID: string, videoID: string, UUID: st
});
}
async function sendWebhooks(apiVideoInfo: APIVideoInfo, userID: string, videoID: string, UUID: string, segmentInfo: any, service: Service) {
if (apiVideoInfo && service == Service.YouTube) {
async function sendWebhooks(apiVideoDetails: videoDetails, userID: string, videoID: string, UUID: string, segmentInfo: any, service: Service) {
if (apiVideoDetails && service == Service.YouTube) {
const userSubmissionCountRow = await db.prepare("get", `SELECT count(*) as "submissionCount" FROM "sponsorTimes" WHERE "userID" = ?`, [userID]);
const { data, err } = apiVideoInfo;
if (err) return;
const startTime = parseFloat(segmentInfo.segment[0]);
const endTime = parseFloat(segmentInfo.segment[1]);
sendWebhookNotification(userID, videoID, UUID, userSubmissionCountRow.submissionCount, data, {
sendWebhookNotification(userID, videoID, UUID, userSubmissionCountRow.submissionCount, apiVideoDetails, {
submissionStart: startTime,
submissionEnd: endTime,
}, segmentInfo).catch(Logger.error);
@ -84,7 +81,7 @@ async function sendWebhooks(apiVideoInfo: APIVideoInfo, userID: string, videoID:
axios.post(config.discordFirstTimeSubmissionsWebhookURL, {
embeds: [{
title: data?.title,
title: apiVideoDetails.title,
url: `https://www.youtube.com/watch?v=${videoID}&t=${(parseInt(startTime.toFixed(0)) - 2)}s#requiredSegment=${UUID}`,
description: `Submission ID: ${UUID}\
\n\nTimestamp: \
@ -95,7 +92,7 @@ async function sendWebhooks(apiVideoInfo: APIVideoInfo, userID: string, videoID:
name: userID,
},
thumbnail: {
url: getMaxResThumbnail(data) || "",
url: getMaxResThumbnail(videoID),
},
}],
})
@ -120,18 +117,10 @@ async function sendWebhooks(apiVideoInfo: APIVideoInfo, userID: string, videoID:
// Looks like this was broken for no defined youtube key - fixed but IMO we shouldn't return
// false for a pass - it was confusing and lead to this bug - any use of this function in
// the future could have the same problem.
async function autoModerateSubmission(apiVideoInfo: APIVideoInfo,
async function autoModerateSubmission(apiVideoDetails: videoDetails,
submission: { videoID: VideoID; userID: UserID; segments: IncomingSegment[], service: Service, videoDuration: number }) {
const apiVideoDuration = (apiVideoInfo: APIVideoInfo) => {
if (!apiVideoInfo) return undefined;
const { err, data } = apiVideoInfo;
// return undefined if API error
if (err) return undefined;
return data?.lengthSeconds;
};
// get duration from API
const apiDuration = apiVideoDuration(apiVideoInfo);
const apiDuration = apiVideoDetails.duration;
// if API fail or returns 0, get duration from client
const duration = apiDuration || submission.videoDuration;
// return false on undefined or 0
@ -165,14 +154,6 @@ async function autoModerateSubmission(apiVideoInfo: APIVideoInfo,
return false;
}
function getYouTubeVideoInfo(videoID: VideoID, ignoreCache = false): Promise<APIVideoInfo> {
if (config.newLeafURLs !== null) {
return YouTubeAPI.listVideos(videoID, ignoreCache);
} else {
return null;
}
}
async function checkUserActiveWarning(userID: string): Promise<CheckResult> {
const MILLISECONDS_IN_HOUR = 3600000;
const now = Date.now();
@ -345,10 +326,10 @@ async function checkEachSegmentValid(rawIP: IPAddress, paramUserID: UserID, user
return CHECK_PASS;
}
async function checkByAutoModerator(videoID: any, userID: any, segments: Array<any>, service:string, apiVideoInfo: APIVideoInfo, videoDuration: number): Promise<CheckResult> {
async function checkByAutoModerator(videoID: any, userID: any, segments: Array<any>, service:string, apiVideoDetails: videoDetails, videoDuration: number): Promise<CheckResult> {
// Auto moderator check
if (service == Service.YouTube) {
const autoModerateResult = await autoModerateSubmission(apiVideoInfo, { userID, videoID, segments, service, videoDuration });
const autoModerateResult = await autoModerateSubmission(apiVideoDetails, { userID, videoID, segments, service, videoDuration });
if (autoModerateResult) {
return {
pass: false,
@ -377,12 +358,13 @@ async function updateDataIfVideoDurationChange(videoID: VideoID, service: Servic
const videoDurationChanged = (videoDuration: number) => videoDuration != 0
&& previousSubmissions.length > 0 && !previousSubmissions.some((e) => Math.abs(videoDuration - e.videoDuration) < 2);
let apiVideoInfo: APIVideoInfo = null;
let apiVideoDetails: videoDetails = null;
if (service == Service.YouTube) {
// Don't use cache if we don't know the video duration, or the client claims that it has changed
apiVideoInfo = await getYouTubeVideoInfo(videoID, !videoDurationParam || previousSubmissions.length === 0 || videoDurationChanged(videoDurationParam));
const ignoreCache = !videoDurationParam || previousSubmissions.length === 0 || videoDurationChanged(videoDurationParam);
apiVideoDetails = await getVideoDetails(videoID, ignoreCache);
}
const apiVideoDuration = apiVideoInfo?.data?.lengthSeconds as VideoDuration;
const apiVideoDuration = apiVideoDetails?.duration as VideoDuration;
if (!videoDurationParam || (apiVideoDuration && Math.abs(videoDurationParam - apiVideoDuration) > 2)) {
// If api duration is far off, take that one instead (it is only precise to seconds, not millis)
videoDuration = apiVideoDuration || 0 as VideoDuration;
@ -400,7 +382,7 @@ async function updateDataIfVideoDurationChange(videoID: VideoID, service: Servic
return {
videoDuration,
apiVideoInfo,
apiVideoDetails,
lockedCategoryList
};
}
@ -501,10 +483,6 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
//hash the userID
const userID = await getHashCache(paramUserID || "");
if (userID === "a41d853c7328a86f8d712f910c4ef77f6c7a9e467f349781b1a7d405c37b681b") {
return res.status(200);
}
const invalidCheckResult = await checkInvalidFields(videoID, paramUserID, userID, segments, videoDurationParam, userAgent);
if (!invalidCheckResult.pass) {
return res.status(invalidCheckResult.errorCode).send(invalidCheckResult.errorMessage);
@ -521,7 +499,7 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
const newData = await updateDataIfVideoDurationChange(videoID, service, videoDuration, videoDurationParam);
videoDuration = newData.videoDuration;
const { lockedCategoryList, apiVideoInfo } = newData;
const { lockedCategoryList, apiVideoDetails } = newData;
// Check if all submissions are correct
const segmentCheckResult = await checkEachSegmentValid(rawIP, paramUserID, userID, videoID, segments, service, isVIP, lockedCategoryList);
@ -530,7 +508,7 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
}
if (!isVIP) {
const autoModerateCheckResult = await checkByAutoModerator(videoID, userID, segments, service, apiVideoInfo, videoDurationParam);
const autoModerateCheckResult = await checkByAutoModerator(videoID, userID, segments, service, apiVideoDetails, videoDurationParam);
if (!autoModerateCheckResult.pass) {
return res.status(autoModerateCheckResult.errorCode).send(autoModerateCheckResult.errorMessage);
}
@ -583,10 +561,10 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
//add to private db as well
await privateDB.prepare("run", `INSERT INTO "sponsorTimes" VALUES(?, ?, ?, ?)`, [videoID, hashedIP, timeSubmitted, service]);
await db.prepare("run", `INSERT INTO "videoInfo" ("videoID", "channelID", "title", "published", "genreUrl")
SELECT ?, ?, ?, ?, ?
await db.prepare("run", `INSERT INTO "videoInfo" ("videoID", "channelID", "title", "published")
SELECT ?, ?, ?, ?
WHERE NOT EXISTS (SELECT 1 FROM "videoInfo" WHERE "videoID" = ?)`, [
videoID, apiVideoInfo?.data?.authorId || "", apiVideoInfo?.data?.title || "", apiVideoInfo?.data?.published || 0, apiVideoInfo?.data?.genreUrl || "", videoID]);
videoID, apiVideoDetails?.authorId || "", apiVideoDetails?.title || "", apiVideoDetails?.published || 0, videoID]);
// Clear redis cache for this video
QueryCacher.clearSegmentCache({
@ -614,7 +592,7 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
}
for (let i = 0; i < segments.length; i++) {
sendWebhooks(apiVideoInfo, userID, videoID, UUIDs[i], segments[i], service).catch(Logger.error);
sendWebhooks(apiVideoDetails, userID, videoID, UUIDs[i], segments[i], service).catch(Logger.error);
}
return res.json(newSegments);
}

View file

@ -3,7 +3,6 @@ import { Logger } from "../utils/logger";
import { isUserVIP } from "../utils/isUserVIP";
import { isUserTempVIP } from "../utils/isUserTempVIP";
import { getMaxResThumbnail, YouTubeAPI } from "../utils/youtubeApi";
import { APIVideoInfo } from "../types/youtubeApi.model";
import { db, privateDB } from "../databases/databases";
import { dispatchEvent, getVoteAuthor, getVoteAuthorRaw } from "../utils/webhookUtils";
import { getFormattedTime } from "../utils/getFormattedTime";
@ -14,6 +13,7 @@ import { UserID } from "../types/user.model";
import { DBSegment, Category, HashedIP, IPAddress, SegmentUUID, Service, VideoID, VideoIDHash, VideoDuration, ActionType, VoteType } from "../types/segments.model";
import { QueryCacher } from "../utils/queryCacher";
import axios from "axios";
import { getVideoDetails, videoDetails } from "../utils/getVideoDetails";
const voteTypes = {
normal: 0,
@ -52,20 +52,16 @@ interface VoteData {
finalResponse: FinalResponse;
}
function getYouTubeVideoInfo(videoID: VideoID, ignoreCache = false): Promise<APIVideoInfo> {
return config.newLeafURLs ? YouTubeAPI.listVideos(videoID, ignoreCache) : null;
}
const videoDurationChanged = (segmentDuration: number, APIDuration: number) => (APIDuration > 0 && Math.abs(segmentDuration - APIDuration) > 2);
async function updateSegmentVideoDuration(UUID: SegmentUUID) {
const { videoDuration, videoID, service } = await db.prepare("get", `select "videoDuration", "videoID", "service" from "sponsorTimes" where "UUID" = ?`, [UUID]);
let apiVideoInfo: APIVideoInfo = null;
let apiVideoDetails: videoDetails = null;
if (service == Service.YouTube) {
// don't use cache since we have no information about the video length
apiVideoInfo = await getYouTubeVideoInfo(videoID);
apiVideoDetails = await getVideoDetails(videoID);
}
const apiVideoDuration = apiVideoInfo?.data?.lengthSeconds as VideoDuration;
const apiVideoDuration = apiVideoDetails?.duration as VideoDuration;
if (videoDurationChanged(videoDuration, apiVideoDuration)) {
Logger.info(`Video duration changed for ${videoID} from ${videoDuration} to ${apiVideoDuration}`);
await db.prepare("run", `UPDATE "sponsorTimes" SET "videoDuration" = ? WHERE "UUID" = ?`, [apiVideoDuration, UUID]);
@ -74,12 +70,12 @@ async function updateSegmentVideoDuration(UUID: SegmentUUID) {
async function checkVideoDuration(UUID: SegmentUUID) {
const { videoID, service } = await db.prepare("get", `select "videoID", "service" from "sponsorTimes" where "UUID" = ?`, [UUID]);
let apiVideoInfo: APIVideoInfo = null;
let apiVideoDetails: videoDetails = null;
if (service == Service.YouTube) {
// don't use cache since we have no information about the video length
apiVideoInfo = await getYouTubeVideoInfo(videoID, true);
apiVideoDetails = await getVideoDetails(videoID, true);
}
const apiVideoDuration = apiVideoInfo?.data?.lengthSeconds as VideoDuration;
const apiVideoDuration = apiVideoDetails?.duration as VideoDuration;
// if no videoDuration return early
if (isNaN(apiVideoDuration)) return;
// fetch latest submission
@ -129,7 +125,8 @@ async function sendWebhooks(voteData: VoteData) {
}
if (config.newLeafURLs !== null) {
const { err, data } = await YouTubeAPI.listVideos(submissionInfoRow.videoID);
const videoID = submissionInfoRow.videoID;
const { err, data } = await YouTubeAPI.listVideos(videoID);
if (err) return;
const isUpvote = voteData.incrementAmount > 0;
@ -141,8 +138,8 @@ async function sendWebhooks(voteData: VoteData) {
"video": {
"id": submissionInfoRow.videoID,
"title": data?.title,
"url": `https://www.youtube.com/watch?v=${submissionInfoRow.videoID}`,
"thumbnail": getMaxResThumbnail(data) || null,
"url": `https://www.youtube.com/watch?v=${videoID}`,
"thumbnail": getMaxResThumbnail(videoID),
},
"submission": {
"UUID": voteData.UUID,
@ -187,7 +184,7 @@ async function sendWebhooks(voteData: VoteData) {
`${getVoteAuthor(userSubmissionCountRow.submissionCount, voteData.isTempVIP, voteData.isVIP, voteData.isOwnSubmission)}${voteData.row.locked ? " (Locked)" : ""}`,
},
"thumbnail": {
"url": getMaxResThumbnail(data) || "",
"url": getMaxResThumbnail(videoID),
},
}],
})

View file

@ -19,5 +19,6 @@ export interface innerTubeVideoDetails {
"author": string,
"isPrivate": boolean,
"isUnpluggedCorpus": boolean,
"isLiveContent": boolean
"isLiveContent": boolean,
"publishDate": string
}

View file

@ -0,0 +1,59 @@
import { config } from "../config";
import { innerTubeVideoDetails } from "../types/innerTubeApi.model";
import { APIVideoData } from "../types/youtubeApi.model";
import { YouTubeAPI } from "../utils/youtubeApi";
import { getPlayerData } from "../utils/innerTubeAPI";
export interface videoDetails {
videoId: string,
duration: number,
authorId: string,
authorName: string,
title: string,
published: number,
thumbnails: {
url: string,
width: number,
height: number,
}[]
}
const convertFromInnerTube = (input: innerTubeVideoDetails): videoDetails => ({
videoId: input.videoId,
duration: Number(input.lengthSeconds),
authorId: input.channelId,
authorName: input.author,
title: input.title,
published: new Date(input.publishDate).getTime()/1000,
thumbnails: input.thumbnail.thumbnails
});
const convertFromNewLeaf = (input: APIVideoData): videoDetails => ({
videoId: input.videoId,
duration: input.lengthSeconds,
authorId: input.authorId,
authorName: input.author,
title: input.title,
published: input.published,
thumbnails: input.videoThumbnails
});
async function newLeafWrapper(videoId: string, ignoreCache: boolean) {
const result = await YouTubeAPI.listVideos(videoId, ignoreCache);
return result?.data ?? Promise.reject();
}
export function getVideoDetails(videoId: string, ignoreCache = false): Promise<videoDetails> {
if (!config.newLeafURLs) {
return getPlayerData(videoId)
.then(data => convertFromInnerTube(data));
}
return Promise.any([
newLeafWrapper(videoId, ignoreCache)
.then(videoData => convertFromNewLeaf(videoData)),
getPlayerData(videoId)
.then(data => convertFromInnerTube(data))
]).catch(() => {
return null;
});
}

View file

@ -22,8 +22,3 @@ export async function getPlayerData(videoID: string): Promise<innerTubeVideoDeta
return Promise.reject(result.status);
}
}
export const getLength = (videoID: string): Promise<number> =>
getPlayerData(videoID)
.then(pData => Number(pData.lengthSeconds))
.catch(err => err);

View file

@ -1,19 +1,13 @@
import redis from "../utils/redis";
import { tempVIPKey } from "../utils/redisKeys";
import { HashedUserID } from "../types/user.model";
import { YouTubeAPI } from "../utils/youtubeApi";
import { APIVideoInfo } from "../types/youtubeApi.model";
import { VideoID } from "../types/segments.model";
import { config } from "../config";
import { Logger } from "./logger";
function getYouTubeVideoInfo(videoID: VideoID, ignoreCache = false): Promise<APIVideoInfo> {
return config.newLeafURLs ? YouTubeAPI.listVideos(videoID, ignoreCache) : null;
}
import { getVideoDetails } from "./getVideoDetails";
export const isUserTempVIP = async (hashedUserID: HashedUserID, videoID: VideoID): Promise<boolean> => {
const apiVideoInfo = await getYouTubeVideoInfo(videoID);
const channelID = apiVideoInfo?.data?.authorId;
const apiVideoDetails = await getVideoDetails(videoID);
const channelID = apiVideoDetails?.authorId;
try {
const reply = await redis.get(tempVIPKey(hashedUserID));
return reply && reply == channelID;

View file

@ -52,6 +52,5 @@ export class YouTubeAPI {
}
}
export function getMaxResThumbnail(apiInfo: APIVideoData): string | void {
return apiInfo?.videoThumbnails?.find((elem) => elem.quality === "maxres")?.second__originalUrl;
}
export const getMaxResThumbnail = (videoID: string): string =>
`https://i.ytimg.com/vi/${videoID}/maxresdefault.jpg`;

View file

@ -19,9 +19,9 @@ if (db instanceof Postgres) {
await db.prepare("run", query, [chapterNamesVid1, 70, 75, 2, 0, "chapterNamesVid-2", "testman", 0, 50, "chapter", "chapter", "YouTube", 0, 0, 0, "A different one"]);
await db.prepare("run", query, [chapterNamesVid1, 71, 76, 2, 0, "chapterNamesVid-3", "testman", 0, 50, "chapter", "chapter", "YouTube", 0, 0, 0, "Something else"]);
await db.prepare("run", `INSERT INTO "videoInfo" ("videoID", "channelID", "title", "published", "genreUrl")
SELECT ?, ?, ?, ?, ?`, [
chapterNamesVid1, chapterChannelID, "", 0, ""
await db.prepare("run", `INSERT INTO "videoInfo" ("videoID", "channelID", "title", "published")
SELECT ?, ?, ?, ?`, [
chapterNamesVid1, chapterChannelID, "", 0
]);
});

View file

@ -4,10 +4,10 @@ import assert from "assert";
import { YouTubeAPI } from "../../src/utils/youtubeApi";
import * as innerTube from "../../src/utils/innerTubeAPI";
import { partialDeepEquals } from "../utils/partialDeepEquals";
import { getVideoDetails } from "../../src/utils/getVideoDetails";
const videoID = "dQw4w9WgXcQ";
const expected = { // partial type of innerTubeVideoDetails
const expectedInnerTube = { // partial type of innerTubeVideoDetails
videoId: videoID,
title: "Rick Astley - Never Gonna Give You Up (Official Music Video)",
lengthSeconds: "212",
@ -25,17 +25,12 @@ const currentViews = 1284257550;
describe("innertube API test", function() {
it("should be able to get innerTube details", async () => {
const result = await innerTube.getPlayerData(videoID);
assert.ok(partialDeepEquals(result, expected));
assert.ok(partialDeepEquals(result, expectedInnerTube));
});
it("Should have more views than current", async () => {
const result = await innerTube.getPlayerData(videoID);
assert.ok(Number(result.viewCount) >= currentViews);
});
it("Should have the same video duration from both endpoints", async () => {
const playerData = await innerTube.getPlayerData(videoID);
const length = await innerTube.getLength(videoID);
assert.equal(Number(playerData.lengthSeconds), length);
});
it("Should have equivalent response from NewLeaf", async function () {
if (!config.newLeafURLs || config.newLeafURLs.length <= 0 || config.newLeafURLs[0] == "placeholder") this.skip();
const itResponse = await innerTube.getPlayerData(videoID);
@ -48,4 +43,8 @@ describe("innertube API test", function() {
// validate authorId
assert.strictEqual(itResponse.channelId, newLeafResponse.data?.authorId);
});
it("Should return data from generic endpoint", async function () {
const videoDetail = await getVideoDetails(videoID);
assert.ok(videoDetail);
});
});

View file

@ -141,7 +141,6 @@ describe("postSkipSegments", () => {
title: "Example Title",
channelID: "ExampleChannel",
published: 123,
genreUrl: ""
};
assert.ok(partialDeepEquals(videoInfo, expectedVideoInfo));

View file

@ -15,7 +15,7 @@ export class YouTubeApiMock {
if (obj.id === "noDuration" || obj.id === "full_video_duration_segment") {
return {
err: null,
err: false,
data: {
title: "Example Title",
lengthSeconds: 0,
@ -32,7 +32,7 @@ export class YouTubeApiMock {
};
} else if (obj.id === "duration-update") {
return {
err: null,
err: false,
data: {
title: "Example Title",
lengthSeconds: 500,
@ -49,7 +49,7 @@ export class YouTubeApiMock {
};
} else if (obj.id === "channelid-convert") {
return {
err: null,
err: false,
data: {
title: "Video Lookup Title",
author: "ChannelAuthor",
@ -58,14 +58,14 @@ export class YouTubeApiMock {
};
} else if (obj.id === "duration-changed") {
return {
err: null,
err: false,
data: {
lengthSeconds: 100,
} as APIVideoData
};
} else {
return {
err: null,
err: false,
data: {
title: "Example Title",
authorId: "ExampleChannel",

View file

@ -4,7 +4,7 @@
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es2016",
"target": "ES2021",
/* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "commonjs",
/* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */