Add typing to caption api logic (#62)
This commit is contained in:
parent
cdf93feb25
commit
865c22e1fd
3 changed files with 70 additions and 51 deletions
|
@ -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",
|
||||
|
|
|
@ -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}`);
|
||||
|
|
|
@ -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;
|
||||
|
|
Reference in a new issue