Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 40 additions & 8 deletions airtable/utils/ui-templates/selection-page.template.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@

body {
font-family:
Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Inter,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
sans-serif;
margin: 0;
padding: 20px;
Expand Down Expand Up @@ -90,7 +94,11 @@
text-align: left;
margin: 0 16px;
font-family:
Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Inter,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
sans-serif;
}

Expand Down Expand Up @@ -148,7 +156,11 @@
font-size: 14px;
text-align: left;
font-family:
Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Inter,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
sans-serif;
}

Expand Down Expand Up @@ -183,7 +195,11 @@
color: #f5f5f4;
transition: border-color 0.2s;
font-family:
Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Inter,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
sans-serif;
}

Expand Down Expand Up @@ -336,7 +352,11 @@
font-size: 14px;
color: #f5f5f4;
font-family:
Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Inter,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
sans-serif;
}

Expand Down Expand Up @@ -471,7 +491,11 @@
font-size: 14px;
color: #f5f5f4;
font-family:
Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Inter,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
sans-serif;
}

Expand All @@ -486,7 +510,11 @@
color: #737373;
font-size: 14px;
font-family:
Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Inter,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
sans-serif;
}

Expand Down Expand Up @@ -519,7 +547,11 @@
align-items: center;
justify-content: center;
font-family:
Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Inter,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
sans-serif;
}

Expand Down
145 changes: 145 additions & 0 deletions google-youtube/loaders/videos/captions/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { AppContext } from "../../../mod.ts";

export interface Props {
/**
* @title Caption ID
* @description ID of the caption track to download
*/
id: string;

/**
* @title Format
* @description Output format for the caption
*/
tfmt?: "srt" | "vtt";

/**
* @title Translation Language
* @description ISO 639-1 language code for caption translation
*/
tlang?: string;

/**
* @title On Behalf Of Content Owner
* @description Parameter for YouTube content partners
*/
onBehalfOfContentOwner?: string;
}

/**
* @name GET_CAPTION
* @title Download Caption Track
* @description Downloads a caption track in the specified format and language. If token issues occur, call another YouTube tool first to refresh the token.
*/
export default async function get(
props: Props,
_req: Request,
ctx: AppContext,
) {
const { id, tfmt = "vtt", tlang, onBehalfOfContentOwner } = props;

try {
if (!ctx.tokens?.access_token) {
throw new Error(
"Authentication required. Please authenticate with YouTube first.",
);
}

const accessToken = ctx.tokens.access_token;

let url = `https://www.googleapis.com/youtube/v3/captions/${id}`;

const params = new URLSearchParams();
if (tfmt) params.append("tfmt", tfmt);
if (tlang) params.append("tlang", tlang);
if (onBehalfOfContentOwner) {
params.append("onBehalfOfContentOwner", onBehalfOfContentOwner);
}

const queryString = params.toString();
if (queryString) {
url += `?${queryString}`;
}

const response = await fetch(url, {
headers: {
"Authorization": `Bearer ${accessToken}`,
},
});

if (!response.ok) {
if (response.status === 401) {
throw new Error(
"Authentication token expired or invalid. Please refresh your YouTube authentication.",
);
}

if (response.status === 400 && tfmt !== "vtt") {
const fallbackParams = new URLSearchParams();
fallbackParams.append("tfmt", "vtt");
if (tlang) fallbackParams.append("tlang", tlang);
if (onBehalfOfContentOwner) {
fallbackParams.append(
"onBehalfOfContentOwner",
onBehalfOfContentOwner,
);
}

const fallbackUrl =
`https://www.googleapis.com/youtube/v3/captions/${id}?${fallbackParams.toString()}`;

const fallbackResponse = await fetch(fallbackUrl, {
headers: {
"Authorization": `Bearer ${accessToken}`,
},
});

if (!fallbackResponse.ok) {
let errorDetails = "";
try {
const errorJson = await fallbackResponse.json();
errorDetails = JSON.stringify(errorJson);
} catch {
errorDetails = `Status code: ${fallbackResponse.status}`;
}

throw new Error(
`Failed to fetch caption with fallback format: ${errorDetails}`,
);
}

return await fallbackResponse.text();
}

let errorDetails = "";
try {
const errorJson = await response.json();
errorDetails = JSON.stringify(errorJson);
} catch {
errorDetails = `Status code: ${response.status}`;
}

throw new Error(`Failed to fetch caption: ${errorDetails}`);
}

return await response.text();
} catch (error: unknown) {
if (
error instanceof Error &&
(error.message.includes("Authentication required") ||
error.message.includes("token expired") ||
error.message.includes("invalid"))
) {
ctx.errorHandler.toHttpError(
error,
error.message,
401,
);
}

ctx.errorHandler.toHttpError(
error,
`Error fetching caption with ID ${id}`,
);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AppContext } from "../../mod.ts";
import { YouTubeCaptionListResponse } from "../../utils/types.ts";
import { COMMON_ERROR_MESSAGES } from "../../utils/constant.ts";
import { AppContext } from "../../../mod.ts";
import { YouTubeCaptionListResponse } from "../../../utils/types.ts";
import { COMMON_ERROR_MESSAGES } from "../../../utils/constant.ts";

export interface Props {
/**
Expand All @@ -21,18 +21,6 @@ export interface Props {
*/
format?: "srt" | "sbv" | "vtt";

/**
* @title Translation Language
* @description Language code for translation
*/
translationLanguage?: string;

/**
* @title Preferred Language
* @description Preferred language code to fetch caption directly
*/
preferredLanguage?: string;

/**
* @title Auto Load Caption
* @description If true, automatically loads the first available caption
Expand Down Expand Up @@ -69,8 +57,6 @@ const loader = async (
videoId,
captionId,
format = "srt",
translationLanguage,
preferredLanguage,
autoLoadCaption = true,
} = props;

Expand All @@ -82,27 +68,14 @@ const loader = async (
}

try {
const captionsListResponse = await ctx.client["GET /captions"](
{
part: "snippet",
videoId,
},
{
headers: ctx.tokens?.access_token
? { Authorization: `Bearer ${ctx.tokens.access_token}` }
: {},
},
);

if (!captionsListResponse.ok) {
ctx.errorHandler.toHttpError(
captionsListResponse,
`Failed to fetch captions list: ${captionsListResponse.statusText}`,
const captionsListResponse = await ctx.invoke["google-youtube"].loaders
.videos.captions.list(
{
videoId,
},
);
}

const captionsList = await captionsListResponse
.json() as YouTubeCaptionListResponse;
const captionsList = captionsListResponse;

const response: CaptionResponse = {
available: captionsList.items?.length > 0
Expand All @@ -111,15 +84,14 @@ const loader = async (
};

const targetCaptionId = captionId ||
findCaptionToLoad(response.available, preferredLanguage, autoLoadCaption);
findCaptionToLoad(response.available, autoLoadCaption);

if (targetCaptionId) {
await loadCaption(
ctx,
response,
targetCaptionId,
format,
translationLanguage,
);
}

Expand All @@ -134,18 +106,10 @@ const loader = async (

function findCaptionToLoad(
available: YouTubeCaptionListResponse,
preferredLanguage?: string,
autoLoadCaption = true,
): string | undefined {
if (!autoLoadCaption || !available.items?.length) return undefined;

if (preferredLanguage) {
const preferredCaption = available.items.find(
(item) => item.snippet.language.startsWith(preferredLanguage),
);
if (preferredCaption) return preferredCaption.id;
}

return available.items[0]?.id;
}

Expand All @@ -154,31 +118,13 @@ async function loadCaption(
response: CaptionResponse,
captionId: string,
format: "srt" | "sbv" | "vtt",
translationLanguage: string | undefined,
): Promise<void> {
try {
const captionResponse = await ctx.client["GET /captions/:id"](
{
const captionText = await ctx.invoke["google-youtube"].loaders.videos
.captions.get({
id: captionId,
tfmt: format,
tlang: translationLanguage,
},
{
headers: ctx.tokens?.access_token
? { Authorization: `Bearer ${ctx.tokens.access_token}` }
: {},
},
);

if (!captionResponse.ok) {
ctx.errorHandler.toHttpError(
captionResponse,
`Failed to fetch caption text: ${captionResponse.statusText}`,
);
return;
}
});

const captionText = await captionResponse.text();
response.loadedCaptionId = captionId;
response.format = format;

Expand Down
Loading
Loading