mirror of
https://github.com/ajayyy/SponsorBlockServer.git
synced 2025-12-06 03:26:59 +03:00
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:
@@ -126,7 +126,6 @@
|
|||||||
| channelID | TEXT | not null |
|
| channelID | TEXT | not null |
|
||||||
| title | TEXT | not null |
|
| title | TEXT | not null |
|
||||||
| published | REAL | not null |
|
| published | REAL | not null |
|
||||||
| genreUrl | TEXT | not null |
|
|
||||||
|
|
||||||
| index | field |
|
| index | field |
|
||||||
| -- | :--: |
|
| -- | :--: |
|
||||||
|
|||||||
7
databases/_upgrade_sponsorTimes_34.sql
Normal file
7
databases/_upgrade_sponsorTimes_34.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE "videoInfo" DROP COLUMN "genreUrl";
|
||||||
|
|
||||||
|
UPDATE "config" SET value = 34 WHERE key = 'version';
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import { VideoID } from "../types/segments.model";
|
import { VideoID } from "../types/segments.model";
|
||||||
import { YouTubeAPI } from "../utils/youtubeApi";
|
import { getVideoDetails } from "../utils/getVideoDetails";
|
||||||
import { APIVideoInfo } from "../types/youtubeApi.model";
|
|
||||||
import { config } from "../config";
|
|
||||||
import { getHashCache } from "../utils/getHashCache";
|
import { getHashCache } from "../utils/getHashCache";
|
||||||
import { privateDB } from "../databases/databases";
|
import { privateDB } from "../databases/databases";
|
||||||
import { Request, Response } from "express";
|
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 getChannelInfo = async (videoID: VideoID): Promise<{id: string | null, name: string | null }> => {
|
||||||
const videoInfo = await getYouTubeVideoInfo(videoID);
|
const videoInfo = await getVideoDetails(videoID);
|
||||||
return {
|
return {
|
||||||
id: videoInfo?.data?.authorId,
|
id: videoInfo?.authorId,
|
||||||
name: videoInfo?.data?.author
|
name: videoInfo?.authorName
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { config } from "../config";
|
import { config } from "../config";
|
||||||
import { Logger } from "../utils/logger";
|
import { Logger } from "../utils/logger";
|
||||||
import { db, privateDB } from "../databases/databases";
|
import { db, privateDB } from "../databases/databases";
|
||||||
import { getMaxResThumbnail, YouTubeAPI } from "../utils/youtubeApi";
|
import { getMaxResThumbnail } from "../utils/youtubeApi";
|
||||||
import { getSubmissionUUID } from "../utils/getSubmissionUUID";
|
import { getSubmissionUUID } from "../utils/getSubmissionUUID";
|
||||||
import { getHash } from "../utils/getHash";
|
import { getHash } from "../utils/getHash";
|
||||||
import { getHashCache } from "../utils/getHashCache";
|
import { getHashCache } from "../utils/getHashCache";
|
||||||
@@ -13,7 +13,6 @@ import { ActionType, Category, IncomingSegment, IPAddress, SegmentUUID, Service,
|
|||||||
import { deleteLockCategories } from "./deleteLockCategories";
|
import { deleteLockCategories } from "./deleteLockCategories";
|
||||||
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 { HashedUserID, UserID } from "../types/user.model";
|
import { HashedUserID, UserID } from "../types/user.model";
|
||||||
import { isUserVIP } from "../utils/isUserVIP";
|
import { isUserVIP } from "../utils/isUserVIP";
|
||||||
import { isUserTempVIP } from "../utils/isUserTempVIP";
|
import { isUserTempVIP } from "../utils/isUserTempVIP";
|
||||||
@@ -22,6 +21,7 @@ import { getService } from "../utils/getService";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { vote } from "./voteOnSponsorTime";
|
import { vote } from "./voteOnSponsorTime";
|
||||||
import { canSubmit } from "../utils/permissions";
|
import { canSubmit } from "../utils/permissions";
|
||||||
|
import { getVideoDetails, videoDetails } from "../utils/getVideoDetails";
|
||||||
|
|
||||||
type CheckResult = {
|
type CheckResult = {
|
||||||
pass: boolean,
|
pass: boolean,
|
||||||
@@ -35,7 +35,7 @@ const CHECK_PASS: CheckResult = {
|
|||||||
errorCode: 0
|
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 row = await db.prepare("get", `SELECT "userName" FROM "userNames" WHERE "userID" = ?`, [userID]);
|
||||||
const userName = row !== undefined ? row.userName : null;
|
const userName = row !== undefined ? row.userName : null;
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ async function sendWebhookNotification(userID: string, videoID: string, UUID: st
|
|||||||
"video": {
|
"video": {
|
||||||
"id": videoID,
|
"id": videoID,
|
||||||
"title": youtubeData?.title,
|
"title": youtubeData?.title,
|
||||||
"thumbnail": getMaxResThumbnail(youtubeData) || null,
|
"thumbnail": getMaxResThumbnail(videoID),
|
||||||
"url": `https://www.youtube.com/watch?v=${videoID}`,
|
"url": `https://www.youtube.com/watch?v=${videoID}`,
|
||||||
},
|
},
|
||||||
"submission": {
|
"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) {
|
async function sendWebhooks(apiVideoDetails: videoDetails, userID: string, videoID: string, UUID: string, segmentInfo: any, service: Service) {
|
||||||
if (apiVideoInfo && service == Service.YouTube) {
|
if (apiVideoDetails && service == Service.YouTube) {
|
||||||
const userSubmissionCountRow = await db.prepare("get", `SELECT count(*) as "submissionCount" FROM "sponsorTimes" WHERE "userID" = ?`, [userID]);
|
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 startTime = parseFloat(segmentInfo.segment[0]);
|
||||||
const endTime = parseFloat(segmentInfo.segment[1]);
|
const endTime = parseFloat(segmentInfo.segment[1]);
|
||||||
sendWebhookNotification(userID, videoID, UUID, userSubmissionCountRow.submissionCount, data, {
|
sendWebhookNotification(userID, videoID, UUID, userSubmissionCountRow.submissionCount, apiVideoDetails, {
|
||||||
submissionStart: startTime,
|
submissionStart: startTime,
|
||||||
submissionEnd: endTime,
|
submissionEnd: endTime,
|
||||||
}, segmentInfo).catch(Logger.error);
|
}, segmentInfo).catch(Logger.error);
|
||||||
@@ -84,7 +81,7 @@ async function sendWebhooks(apiVideoInfo: APIVideoInfo, userID: string, videoID:
|
|||||||
|
|
||||||
axios.post(config.discordFirstTimeSubmissionsWebhookURL, {
|
axios.post(config.discordFirstTimeSubmissionsWebhookURL, {
|
||||||
embeds: [{
|
embeds: [{
|
||||||
title: data?.title,
|
title: apiVideoDetails.title,
|
||||||
url: `https://www.youtube.com/watch?v=${videoID}&t=${(parseInt(startTime.toFixed(0)) - 2)}s#requiredSegment=${UUID}`,
|
url: `https://www.youtube.com/watch?v=${videoID}&t=${(parseInt(startTime.toFixed(0)) - 2)}s#requiredSegment=${UUID}`,
|
||||||
description: `Submission ID: ${UUID}\
|
description: `Submission ID: ${UUID}\
|
||||||
\n\nTimestamp: \
|
\n\nTimestamp: \
|
||||||
@@ -95,7 +92,7 @@ async function sendWebhooks(apiVideoInfo: APIVideoInfo, userID: string, videoID:
|
|||||||
name: userID,
|
name: userID,
|
||||||
},
|
},
|
||||||
thumbnail: {
|
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
|
// 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
|
// 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.
|
// 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 }) {
|
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
|
// get duration from API
|
||||||
const apiDuration = apiVideoDuration(apiVideoInfo);
|
const apiDuration = apiVideoDetails.duration;
|
||||||
// if API fail or returns 0, get duration from client
|
// if API fail or returns 0, get duration from client
|
||||||
const duration = apiDuration || submission.videoDuration;
|
const duration = apiDuration || submission.videoDuration;
|
||||||
// return false on undefined or 0
|
// return false on undefined or 0
|
||||||
@@ -165,14 +154,6 @@ async function autoModerateSubmission(apiVideoInfo: APIVideoInfo,
|
|||||||
return false;
|
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> {
|
async function checkUserActiveWarning(userID: string): Promise<CheckResult> {
|
||||||
const MILLISECONDS_IN_HOUR = 3600000;
|
const MILLISECONDS_IN_HOUR = 3600000;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -345,10 +326,10 @@ async function checkEachSegmentValid(rawIP: IPAddress, paramUserID: UserID, user
|
|||||||
return CHECK_PASS;
|
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
|
// Auto moderator check
|
||||||
if (service == Service.YouTube) {
|
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) {
|
if (autoModerateResult) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: false,
|
||||||
@@ -377,12 +358,13 @@ async function updateDataIfVideoDurationChange(videoID: VideoID, service: Servic
|
|||||||
const videoDurationChanged = (videoDuration: number) => videoDuration != 0
|
const videoDurationChanged = (videoDuration: number) => videoDuration != 0
|
||||||
&& previousSubmissions.length > 0 && !previousSubmissions.some((e) => Math.abs(videoDuration - e.videoDuration) < 2);
|
&& previousSubmissions.length > 0 && !previousSubmissions.some((e) => Math.abs(videoDuration - e.videoDuration) < 2);
|
||||||
|
|
||||||
let apiVideoInfo: APIVideoInfo = null;
|
let apiVideoDetails: videoDetails = null;
|
||||||
if (service == Service.YouTube) {
|
if (service == Service.YouTube) {
|
||||||
// Don't use cache if we don't know the video duration, or the client claims that it has changed
|
// 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 (!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)
|
// If api duration is far off, take that one instead (it is only precise to seconds, not millis)
|
||||||
videoDuration = apiVideoDuration || 0 as VideoDuration;
|
videoDuration = apiVideoDuration || 0 as VideoDuration;
|
||||||
@@ -400,7 +382,7 @@ async function updateDataIfVideoDurationChange(videoID: VideoID, service: Servic
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
videoDuration,
|
videoDuration,
|
||||||
apiVideoInfo,
|
apiVideoDetails,
|
||||||
lockedCategoryList
|
lockedCategoryList
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -501,10 +483,6 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
|
|||||||
//hash the userID
|
//hash the userID
|
||||||
const userID = await getHashCache(paramUserID || "");
|
const userID = await getHashCache(paramUserID || "");
|
||||||
|
|
||||||
if (userID === "a41d853c7328a86f8d712f910c4ef77f6c7a9e467f349781b1a7d405c37b681b") {
|
|
||||||
return res.status(200);
|
|
||||||
}
|
|
||||||
|
|
||||||
const invalidCheckResult = await checkInvalidFields(videoID, paramUserID, userID, segments, videoDurationParam, userAgent);
|
const invalidCheckResult = await checkInvalidFields(videoID, paramUserID, userID, segments, videoDurationParam, userAgent);
|
||||||
if (!invalidCheckResult.pass) {
|
if (!invalidCheckResult.pass) {
|
||||||
return res.status(invalidCheckResult.errorCode).send(invalidCheckResult.errorMessage);
|
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);
|
const newData = await updateDataIfVideoDurationChange(videoID, service, videoDuration, videoDurationParam);
|
||||||
videoDuration = newData.videoDuration;
|
videoDuration = newData.videoDuration;
|
||||||
const { lockedCategoryList, apiVideoInfo } = newData;
|
const { lockedCategoryList, apiVideoDetails } = newData;
|
||||||
|
|
||||||
// Check if all submissions are correct
|
// Check if all submissions are correct
|
||||||
const segmentCheckResult = await checkEachSegmentValid(rawIP, paramUserID, userID, videoID, segments, service, isVIP, lockedCategoryList);
|
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) {
|
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) {
|
if (!autoModerateCheckResult.pass) {
|
||||||
return res.status(autoModerateCheckResult.errorCode).send(autoModerateCheckResult.errorMessage);
|
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
|
//add to private db as well
|
||||||
await privateDB.prepare("run", `INSERT INTO "sponsorTimes" VALUES(?, ?, ?, ?)`, [videoID, hashedIP, timeSubmitted, service]);
|
await privateDB.prepare("run", `INSERT INTO "sponsorTimes" VALUES(?, ?, ?, ?)`, [videoID, hashedIP, timeSubmitted, service]);
|
||||||
|
|
||||||
await db.prepare("run", `INSERT INTO "videoInfo" ("videoID", "channelID", "title", "published", "genreUrl")
|
await db.prepare("run", `INSERT INTO "videoInfo" ("videoID", "channelID", "title", "published")
|
||||||
SELECT ?, ?, ?, ?, ?
|
SELECT ?, ?, ?, ?
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM "videoInfo" WHERE "videoID" = ?)`, [
|
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
|
// Clear redis cache for this video
|
||||||
QueryCacher.clearSegmentCache({
|
QueryCacher.clearSegmentCache({
|
||||||
@@ -614,7 +592,7 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < segments.length; i++) {
|
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);
|
return res.json(newSegments);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Logger } from "../utils/logger";
|
|||||||
import { isUserVIP } from "../utils/isUserVIP";
|
import { isUserVIP } from "../utils/isUserVIP";
|
||||||
import { isUserTempVIP } from "../utils/isUserTempVIP";
|
import { isUserTempVIP } from "../utils/isUserTempVIP";
|
||||||
import { getMaxResThumbnail, YouTubeAPI } from "../utils/youtubeApi";
|
import { getMaxResThumbnail, YouTubeAPI } from "../utils/youtubeApi";
|
||||||
import { APIVideoInfo } from "../types/youtubeApi.model";
|
|
||||||
import { db, privateDB } from "../databases/databases";
|
import { db, privateDB } from "../databases/databases";
|
||||||
import { dispatchEvent, getVoteAuthor, getVoteAuthorRaw } from "../utils/webhookUtils";
|
import { dispatchEvent, getVoteAuthor, getVoteAuthorRaw } from "../utils/webhookUtils";
|
||||||
import { getFormattedTime } from "../utils/getFormattedTime";
|
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 { DBSegment, Category, HashedIP, IPAddress, SegmentUUID, Service, VideoID, VideoIDHash, VideoDuration, ActionType, VoteType } from "../types/segments.model";
|
||||||
import { QueryCacher } from "../utils/queryCacher";
|
import { QueryCacher } from "../utils/queryCacher";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import { getVideoDetails, videoDetails } from "../utils/getVideoDetails";
|
||||||
|
|
||||||
const voteTypes = {
|
const voteTypes = {
|
||||||
normal: 0,
|
normal: 0,
|
||||||
@@ -52,20 +52,16 @@ interface VoteData {
|
|||||||
finalResponse: FinalResponse;
|
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);
|
const videoDurationChanged = (segmentDuration: number, APIDuration: number) => (APIDuration > 0 && Math.abs(segmentDuration - APIDuration) > 2);
|
||||||
|
|
||||||
async function updateSegmentVideoDuration(UUID: SegmentUUID) {
|
async function updateSegmentVideoDuration(UUID: SegmentUUID) {
|
||||||
const { videoDuration, videoID, service } = await db.prepare("get", `select "videoDuration", "videoID", "service" from "sponsorTimes" where "UUID" = ?`, [UUID]);
|
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) {
|
if (service == Service.YouTube) {
|
||||||
// don't use cache since we have no information about the video length
|
// 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)) {
|
if (videoDurationChanged(videoDuration, apiVideoDuration)) {
|
||||||
Logger.info(`Video duration changed for ${videoID} from ${videoDuration} to ${apiVideoDuration}`);
|
Logger.info(`Video duration changed for ${videoID} from ${videoDuration} to ${apiVideoDuration}`);
|
||||||
await db.prepare("run", `UPDATE "sponsorTimes" SET "videoDuration" = ? WHERE "UUID" = ?`, [apiVideoDuration, UUID]);
|
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) {
|
async function checkVideoDuration(UUID: SegmentUUID) {
|
||||||
const { videoID, service } = await db.prepare("get", `select "videoID", "service" from "sponsorTimes" where "UUID" = ?`, [UUID]);
|
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) {
|
if (service == Service.YouTube) {
|
||||||
// don't use cache since we have no information about the video length
|
// 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 no videoDuration return early
|
||||||
if (isNaN(apiVideoDuration)) return;
|
if (isNaN(apiVideoDuration)) return;
|
||||||
// fetch latest submission
|
// fetch latest submission
|
||||||
@@ -129,7 +125,8 @@ async function sendWebhooks(voteData: VoteData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (config.newLeafURLs !== null) {
|
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;
|
if (err) return;
|
||||||
|
|
||||||
const isUpvote = voteData.incrementAmount > 0;
|
const isUpvote = voteData.incrementAmount > 0;
|
||||||
@@ -141,8 +138,8 @@ async function sendWebhooks(voteData: VoteData) {
|
|||||||
"video": {
|
"video": {
|
||||||
"id": submissionInfoRow.videoID,
|
"id": submissionInfoRow.videoID,
|
||||||
"title": data?.title,
|
"title": data?.title,
|
||||||
"url": `https://www.youtube.com/watch?v=${submissionInfoRow.videoID}`,
|
"url": `https://www.youtube.com/watch?v=${videoID}`,
|
||||||
"thumbnail": getMaxResThumbnail(data) || null,
|
"thumbnail": getMaxResThumbnail(videoID),
|
||||||
},
|
},
|
||||||
"submission": {
|
"submission": {
|
||||||
"UUID": voteData.UUID,
|
"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)" : ""}`,
|
`${getVoteAuthor(userSubmissionCountRow.submissionCount, voteData.isTempVIP, voteData.isVIP, voteData.isOwnSubmission)}${voteData.row.locked ? " (Locked)" : ""}`,
|
||||||
},
|
},
|
||||||
"thumbnail": {
|
"thumbnail": {
|
||||||
"url": getMaxResThumbnail(data) || "",
|
"url": getMaxResThumbnail(videoID),
|
||||||
},
|
},
|
||||||
}],
|
}],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -19,5 +19,6 @@ export interface innerTubeVideoDetails {
|
|||||||
"author": string,
|
"author": string,
|
||||||
"isPrivate": boolean,
|
"isPrivate": boolean,
|
||||||
"isUnpluggedCorpus": boolean,
|
"isUnpluggedCorpus": boolean,
|
||||||
"isLiveContent": boolean
|
"isLiveContent": boolean,
|
||||||
|
"publishDate": string
|
||||||
}
|
}
|
||||||
59
src/utils/getVideoDetails.ts
Normal file
59
src/utils/getVideoDetails.ts
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -21,9 +21,4 @@ export async function getPlayerData(videoID: string): Promise<innerTubeVideoDeta
|
|||||||
} else {
|
} else {
|
||||||
return Promise.reject(result.status);
|
return Promise.reject(result.status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getLength = (videoID: string): Promise<number> =>
|
|
||||||
getPlayerData(videoID)
|
|
||||||
.then(pData => Number(pData.lengthSeconds))
|
|
||||||
.catch(err => err);
|
|
||||||
@@ -1,19 +1,13 @@
|
|||||||
import redis from "../utils/redis";
|
import redis from "../utils/redis";
|
||||||
import { tempVIPKey } from "../utils/redisKeys";
|
import { tempVIPKey } from "../utils/redisKeys";
|
||||||
import { HashedUserID } from "../types/user.model";
|
import { HashedUserID } from "../types/user.model";
|
||||||
import { YouTubeAPI } from "../utils/youtubeApi";
|
|
||||||
import { APIVideoInfo } from "../types/youtubeApi.model";
|
|
||||||
import { VideoID } from "../types/segments.model";
|
import { VideoID } from "../types/segments.model";
|
||||||
import { config } from "../config";
|
|
||||||
import { Logger } from "./logger";
|
import { Logger } from "./logger";
|
||||||
|
import { getVideoDetails } from "./getVideoDetails";
|
||||||
function getYouTubeVideoInfo(videoID: VideoID, ignoreCache = false): Promise<APIVideoInfo> {
|
|
||||||
return config.newLeafURLs ? YouTubeAPI.listVideos(videoID, ignoreCache) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isUserTempVIP = async (hashedUserID: HashedUserID, videoID: VideoID): Promise<boolean> => {
|
export const isUserTempVIP = async (hashedUserID: HashedUserID, videoID: VideoID): Promise<boolean> => {
|
||||||
const apiVideoInfo = await getYouTubeVideoInfo(videoID);
|
const apiVideoDetails = await getVideoDetails(videoID);
|
||||||
const channelID = apiVideoInfo?.data?.authorId;
|
const channelID = apiVideoDetails?.authorId;
|
||||||
try {
|
try {
|
||||||
const reply = await redis.get(tempVIPKey(hashedUserID));
|
const reply = await redis.get(tempVIPKey(hashedUserID));
|
||||||
return reply && reply == channelID;
|
return reply && reply == channelID;
|
||||||
|
|||||||
@@ -52,6 +52,5 @@ export class YouTubeAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMaxResThumbnail(apiInfo: APIVideoData): string | void {
|
export const getMaxResThumbnail = (videoID: string): string =>
|
||||||
return apiInfo?.videoThumbnails?.find((elem) => elem.quality === "maxres")?.second__originalUrl;
|
`https://i.ytimg.com/vi/${videoID}/maxresdefault.jpg`;
|
||||||
}
|
|
||||||
@@ -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, 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", 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")
|
await db.prepare("run", `INSERT INTO "videoInfo" ("videoID", "channelID", "title", "published")
|
||||||
SELECT ?, ?, ?, ?, ?`, [
|
SELECT ?, ?, ?, ?`, [
|
||||||
chapterNamesVid1, chapterChannelID, "", 0, ""
|
chapterNamesVid1, chapterChannelID, "", 0
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import assert from "assert";
|
|||||||
import { YouTubeAPI } from "../../src/utils/youtubeApi";
|
import { YouTubeAPI } from "../../src/utils/youtubeApi";
|
||||||
import * as innerTube from "../../src/utils/innerTubeAPI";
|
import * as innerTube from "../../src/utils/innerTubeAPI";
|
||||||
import { partialDeepEquals } from "../utils/partialDeepEquals";
|
import { partialDeepEquals } from "../utils/partialDeepEquals";
|
||||||
|
import { getVideoDetails } from "../../src/utils/getVideoDetails";
|
||||||
|
|
||||||
const videoID = "dQw4w9WgXcQ";
|
const videoID = "dQw4w9WgXcQ";
|
||||||
const expected = { // partial type of innerTubeVideoDetails
|
const expectedInnerTube = { // partial type of innerTubeVideoDetails
|
||||||
videoId: videoID,
|
videoId: videoID,
|
||||||
title: "Rick Astley - Never Gonna Give You Up (Official Music Video)",
|
title: "Rick Astley - Never Gonna Give You Up (Official Music Video)",
|
||||||
lengthSeconds: "212",
|
lengthSeconds: "212",
|
||||||
@@ -25,17 +25,12 @@ const currentViews = 1284257550;
|
|||||||
describe("innertube API test", function() {
|
describe("innertube API test", function() {
|
||||||
it("should be able to get innerTube details", async () => {
|
it("should be able to get innerTube details", async () => {
|
||||||
const result = await innerTube.getPlayerData(videoID);
|
const result = await innerTube.getPlayerData(videoID);
|
||||||
assert.ok(partialDeepEquals(result, expected));
|
assert.ok(partialDeepEquals(result, expectedInnerTube));
|
||||||
});
|
});
|
||||||
it("Should have more views than current", async () => {
|
it("Should have more views than current", async () => {
|
||||||
const result = await innerTube.getPlayerData(videoID);
|
const result = await innerTube.getPlayerData(videoID);
|
||||||
assert.ok(Number(result.viewCount) >= currentViews);
|
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 () {
|
it("Should have equivalent response from NewLeaf", async function () {
|
||||||
if (!config.newLeafURLs || config.newLeafURLs.length <= 0 || config.newLeafURLs[0] == "placeholder") this.skip();
|
if (!config.newLeafURLs || config.newLeafURLs.length <= 0 || config.newLeafURLs[0] == "placeholder") this.skip();
|
||||||
const itResponse = await innerTube.getPlayerData(videoID);
|
const itResponse = await innerTube.getPlayerData(videoID);
|
||||||
@@ -48,4 +43,8 @@ describe("innertube API test", function() {
|
|||||||
// validate authorId
|
// validate authorId
|
||||||
assert.strictEqual(itResponse.channelId, newLeafResponse.data?.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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -141,7 +141,6 @@ describe("postSkipSegments", () => {
|
|||||||
title: "Example Title",
|
title: "Example Title",
|
||||||
channelID: "ExampleChannel",
|
channelID: "ExampleChannel",
|
||||||
published: 123,
|
published: 123,
|
||||||
genreUrl: ""
|
|
||||||
};
|
};
|
||||||
assert.ok(partialDeepEquals(videoInfo, expectedVideoInfo));
|
assert.ok(partialDeepEquals(videoInfo, expectedVideoInfo));
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export class YouTubeApiMock {
|
|||||||
|
|
||||||
if (obj.id === "noDuration" || obj.id === "full_video_duration_segment") {
|
if (obj.id === "noDuration" || obj.id === "full_video_duration_segment") {
|
||||||
return {
|
return {
|
||||||
err: null,
|
err: false,
|
||||||
data: {
|
data: {
|
||||||
title: "Example Title",
|
title: "Example Title",
|
||||||
lengthSeconds: 0,
|
lengthSeconds: 0,
|
||||||
@@ -32,7 +32,7 @@ export class YouTubeApiMock {
|
|||||||
};
|
};
|
||||||
} else if (obj.id === "duration-update") {
|
} else if (obj.id === "duration-update") {
|
||||||
return {
|
return {
|
||||||
err: null,
|
err: false,
|
||||||
data: {
|
data: {
|
||||||
title: "Example Title",
|
title: "Example Title",
|
||||||
lengthSeconds: 500,
|
lengthSeconds: 500,
|
||||||
@@ -49,7 +49,7 @@ export class YouTubeApiMock {
|
|||||||
};
|
};
|
||||||
} else if (obj.id === "channelid-convert") {
|
} else if (obj.id === "channelid-convert") {
|
||||||
return {
|
return {
|
||||||
err: null,
|
err: false,
|
||||||
data: {
|
data: {
|
||||||
title: "Video Lookup Title",
|
title: "Video Lookup Title",
|
||||||
author: "ChannelAuthor",
|
author: "ChannelAuthor",
|
||||||
@@ -58,14 +58,14 @@ export class YouTubeApiMock {
|
|||||||
};
|
};
|
||||||
} else if (obj.id === "duration-changed") {
|
} else if (obj.id === "duration-changed") {
|
||||||
return {
|
return {
|
||||||
err: null,
|
err: false,
|
||||||
data: {
|
data: {
|
||||||
lengthSeconds: 100,
|
lengthSeconds: 100,
|
||||||
} as APIVideoData
|
} as APIVideoData
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
err: null,
|
err: false,
|
||||||
data: {
|
data: {
|
||||||
title: "Example Title",
|
title: "Example Title",
|
||||||
authorId: "ExampleChannel",
|
authorId: "ExampleChannel",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
/* Basic Options */
|
/* Basic Options */
|
||||||
// "incremental": true, /* Enable incremental compilation */
|
// "incremental": true, /* Enable incremental compilation */
|
||||||
"target": "es2016",
|
"target": "ES2021",
|
||||||
/* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
/* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
/* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
/* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
||||||
|
|||||||
Reference in New Issue
Block a user