Add typing to caption api logic (#62)

This commit is contained in:
syeopite 2025-03-16 14:58:19 +00:00 committed by GitHub
parent cdf93feb25
commit 865c22e1fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 70 additions and 51 deletions

View file

@ -10,6 +10,7 @@
"youtubei.js": "https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v13.1.0-deno/deno.ts",
"youtubei.js/Utils": "https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v13.1.0-deno/deno/src/utils/Utils.ts",
"youtubei.js/NavigationEndpoint": "https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v13.1.0-deno/deno/src/parser/classes/NavigationEndpoint.ts",
"youtubei.js/PlayerCaptionsTracklist": "https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v13.1.0-deno/deno/src/parser/classes/PlayerCaptionsTracklist.ts",
"jsdom": "npm:jsdom@26.0.0",
"bgutils": "https://esm.sh/bgutils-js@3.1.0",
"estree": "https://esm.sh/@types/estree@1.0.6",

View file

@ -1,7 +1,8 @@
import { Innertube } from "youtubei.js";
import type { CaptionTrackData } from "youtubei.js/PlayerCaptionsTracklist";
import { HTTPException } from "hono/http-exception";
// @ts-ignore to be fixed
function createTemporalDuration(milliseconds) {
function createTemporalDuration(milliseconds: number) {
return new Temporal.Duration(
undefined,
undefined,
@ -26,18 +27,18 @@ const ESCAPE_SUBSTITUTIONS = {
export async function handleTranscripts(
innertubeClient: Innertube,
videoId: string,
// @ts-ignore to be fixed
selectedCaption,
selectedCaption: CaptionTrackData,
) {
const lines: string[] = ["WEBVTT"];
const info = await innertubeClient.getInfo(videoId);
const transcriptInfo = await (await info.getTranscript()).selectLanguage(
selectedCaption.name.simpleText,
selectedCaption.name.text || "",
);
const rawTranscriptLines =
// @ts-ignore to be fixed
transcriptInfo.transcript.content.body.initial_segments;
const rawTranscriptLines = transcriptInfo.transcript.content?.body
?.initial_segments;
if (rawTranscriptLines == undefined) throw new HTTPException(404);
rawTranscriptLines.forEach((line) => {
const timestampFormatOptions = {
@ -46,21 +47,38 @@ export async function handleTranscripts(
fractionalDigits: 3,
};
const start_ms = createTemporalDuration(line.start_ms).round({
// Temporal.Duration.prototype.toLocaleString() is supposed to delegate to Intl.DurationFormat
// which Deno does not support. However, instead of following specs and having toLocaleString return
// the same toString() it seems to have its own implementation of Intl.DurationFormat,
// with its options parameter type incorrectly restricted to the same as the one for Intl.DateTimeFormatOptions
// even though they do not share the same arguments.
//
// The above matches the options parameter of Intl.DurationFormat, and the resulting output is as expected.
// Until this is fixed typechecking must be disabled for the two use cases below
//
// See
// https://docs.deno.com/api/web/~/Intl.DateTimeFormatOptions
// https://docs.deno.com/api/web/~/Temporal.Duration.prototype.toLocaleString
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/toLocaleString
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat
const start_ms = createTemporalDuration(Number(line.start_ms)).round({
largestUnit: "year",
// @ts-ignore to be fixed
}).toLocaleString(undefined, timestampFormatOptions);
const end_ms = createTemporalDuration(line.end_ms).round({
//@ts-ignore see above
}).toLocaleString("en-US", timestampFormatOptions);
const end_ms = createTemporalDuration(Number(line.end_ms)).round({
largestUnit: "year",
// @ts-ignore to be fixed
}).toLocaleString(undefined, timestampFormatOptions);
//@ts-ignore see above
}).toLocaleString("en-US", timestampFormatOptions);
const timestamp = `${start_ms} --> ${end_ms}`;
// @ts-ignore to be fixed
const text = line.snippet.text.replace(
const text = (line.snippet?.text || "").replace(
/[&<>\u200E\u200F\u00A0]/g,
// @ts-ignore to be fixed
(match) => ESCAPE_SUBSTITUTIONS[match],
(match: string) =>
ESCAPE_SUBSTITUTIONS[
match as keyof typeof ESCAPE_SUBSTITUTIONS
],
);
lines.push(`${timestamp}\n${text}`);

View file

@ -2,7 +2,11 @@ import { Hono } from "hono";
import type { HonoVariables } from "../../lib/types/HonoVariables.ts";
import { Store } from "@willsoto/node-konfig-core";
import { verifyRequest } from "../../lib/helpers/verifyRequest.ts";
import { youtubePlayerParsing } from "../../lib/helpers/youtubePlayerHandling.ts";
import {
youtubePlayerParsing,
youtubeVideoInfo,
} from "../../lib/helpers/youtubePlayerHandling.ts";
import type { CaptionTrackData } from "youtubei.js/PlayerCaptionsTracklist";
import { handleTranscripts } from "../../lib/helpers/youtubeTranscriptsHandling.ts";
import { HTTPException } from "hono/http-exception";
@ -35,15 +39,19 @@ captionsHandler.get("/:videoId", async (c) => {
const innertubeClient = await c.get("innertubeClient");
const playerJson = await youtubePlayerParsing(
const youtubePlayerResponseJson = await youtubePlayerParsing(
innertubeClient,
videoId,
konfigStore,
);
const captionsTrackArray =
// @ts-ignore to be fixed
playerJson.captions.playerCaptionsTracklistRenderer.captionTracks;
const videoInfo = youtubeVideoInfo(
innertubeClient,
youtubePlayerResponseJson,
);
const captionsTrackArray = videoInfo.captions?.caption_tracks;
if (captionsTrackArray == undefined) throw new HTTPException(404);
const label = c.req.query("label");
const lang = c.req.query("lang");
@ -52,46 +60,38 @@ captionsHandler.get("/:videoId", async (c) => {
if (label == undefined && lang == undefined) {
const invidiousAvailableCaptionsArr: AvailableCaption[] = [];
captionsTrackArray.forEach(
(
captions: {
name: { simpleText: string | number | boolean };
languageCode: any;
},
) => {
invidiousAvailableCaptionsArr.push({
// @ts-ignore to be fixed
label: captions.name.simpleText,
languageCode: captions.languageCode,
url: `/api/v1/captions/${videoId}?label=${
encodeURIComponent(captions.name.simpleText)
}`,
});
},
);
for (const caption_track of captionsTrackArray) {
invidiousAvailableCaptionsArr.push({
label: caption_track.name.text || "",
languageCode: caption_track.language_code,
url: `/api/v1/captions/${videoId}?label=${
encodeURIComponent(caption_track.name.text || "")
}`,
});
}
return c.json({ captions: invidiousAvailableCaptionsArr });
}
// Extract selected caption
let caption;
let filterSelected: CaptionTrackData[];
if (lang) {
// @ts-ignore to be fixed
caption = captionsTrackArray.filter((c) => c.languageCode === lang);
filterSelected = captionsTrackArray.filter((c: CaptionTrackData) =>
c.language_code === lang
);
} else {
// @ts-ignore to be fixed
caption = captionsTrackArray.filter((c) => c.name.simpleText === label);
filterSelected = captionsTrackArray.filter((c: CaptionTrackData) =>
c.name.text === label
);
}
if (caption.length == 0) {
throw new HTTPException(404);
} else {
caption = caption[0];
}
if (filterSelected.length == 0) throw new HTTPException(404);
c.header("Content-Type", "text/vtt; charset=UTF-8");
return c.body(await handleTranscripts(innertubeClient, videoId, caption));
return c.body(
await handleTranscripts(innertubeClient, videoId, filterSelected[0]),
);
});
export default captionsHandler;