Skip to content

Commit

Permalink
Merge pull request #26 from jacob-ian/patch-yt-error
Browse files Browse the repository at this point in the history
  • Loading branch information
jacob-ian authored Jul 11, 2021
2 parents 0ce191c + 4ebb670 commit 01b92d2
Show file tree
Hide file tree
Showing 10 changed files with 5,912 additions and 524 deletions.
5,367 changes: 5,321 additions & 46 deletions functions/package-lock.json

Large diffs are not rendered by default.

11 changes: 9 additions & 2 deletions functions/package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
{
"name": "functions",
"scripts": {
"clean": "rimraf lib/",
"prebuild": "npm run clean",
"build": "tsc",
"serve": "npm run build && DEVELOPMENT=true firebase emulators:start --only functions",
"shell": "npm run build && firebase functions:shell",
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log",
"dev": "tsc && firebase serve --only functions"
"logs": "firebase functions:log"
},
"engines": {
"node": "12"
Expand All @@ -16,8 +17,10 @@
"dependencies": {
"@types/node": "^15.12.5",
"base64url": "^3.0.1",
"express": "^4.17.1",
"fetch-h2": "^2.5.1",
"firebase-admin": "^9.10.0",
"firebase-backend": "^0.2.2",
"firebase-functions": "^3.14.1",
"stream-to-promise": "^3.0.0",
"stripe": "^8.157.0",
Expand All @@ -30,7 +33,11 @@
"@types/stream-to-promise": "^2.2.1",
"@types/urlencode": "^1.1.2",
"@types/xml2js": "^0.4.8",
"@typescript-eslint/eslint-plugin": "^4.28.1",
"@typescript-eslint/parser": "^4.28.1",
"eslint": "^7.29.0",
"firebase-functions-test": "^0.2.3",
"rimraf": "^3.0.2",
"ts-node": "^9.1.1",
"typescript": "^3.9.10"
},
Expand Down
158 changes: 158 additions & 0 deletions functions/src/captions/restful/getCaptionTrack.endpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { Get } from 'firebase-backend';
import { Request, Response } from 'express';
import base64url from 'base64url';
import { context, Response as FetchResponse } from 'fetch-h2';
import { parseStringPromise } from 'xml2js';

const { fetch } = context({
httpProtocol: 'http2',
});

export default new Get(async (req: Request, res: Response) => {
try {
let captionTrack = await getCaptionTrack(req);
return respondWithCaptionTrack(res, captionTrack);
} catch (error) {
return respondWithError(res, error);
}
});

async function getCaptionTrack(req: Request): Promise<string> {
const captionTrackUrl = getCaptionUrlFromRequest(req);
const captionTrackXml = await getCaptionTrackXml(captionTrackUrl);
return captionTrackXml;
}

function getCaptionUrlFromRequest(req: any): string {
const videoQuery = getDecodedVideoQuery(req.query['data']);
const translation = getTranslationParam(req.query['tlang']);

if (!videoQuery) {
throw { code: 400, message: "Missing parameter 'data'." };
}

const captionTrackUrl = createCaptionTrackUrl(videoQuery, translation);
return captionTrackUrl;
}

function getDecodedVideoQuery(data: string | undefined): string | undefined {
try {
return !!data ? base64url.decode(data) : undefined;
} catch (error) {
throw { code: 500, message: 'Could not decode requested video.' };
}
}

function getTranslationParam(tlang: string | undefined): string {
return !!tlang ? `&tlang=${tlang}` : '';
}

function createCaptionTrackUrl(videoQuery: string, translation: string) {
return `https://www.youtube.com/api/timedtext?${videoQuery}${translation}`;
}

async function getCaptionTrackXml(captionTrackUrl: string): Promise<string> {
const readableStream = await getReadableStreamFromUrl(captionTrackUrl);
const unparsedTranscript = await getRawTranscriptFromStream(readableStream);
const xmlParsedTranscript = await parseTranscriptXml(unparsedTranscript);
return xmlParsedTranscript;
}

async function getReadableStreamFromUrl(
location: string
): Promise<NodeJS.ReadableStream> {
try {
let response = await fetch(location);
let readableStream = getReadableFromResponse(response);
return readableStream;
} catch (error) {
throw error;
}
}

async function getReadableFromResponse(
response: FetchResponse
): Promise<NodeJS.ReadableStream> {
if (response.ok) {
return await response.readable();
}
throw { code: response.status, message: await response.json() };
}

async function getRawTranscriptFromStream(
stream: NodeJS.ReadableStream
): Promise<string> {
try {
return await getDataFromStream(stream);
} catch (error) {
throw {
code: 500,
message: "Couldn't download transcript. Try again later.",
};
}
}

function getDataFromStream(stream: NodeJS.ReadableStream): Promise<any> {
stream.setEncoding('utf-8');
return new Promise((resolve, reject) => {
let data = '';
stream.on('data', (chunk) => (data += chunk));
stream.on('end', () => resolve(data));
stream.on('error', (error) => reject(error));
});
}

async function parseTranscriptXml(unparsedXml: string): Promise<string> {
let parsedXml = await parseXmlString(unparsedXml);
if (!parsedXml) {
throw {
code: 500,
message: 'We were unable to create the transcript. Please try again.',
};
}
let transcript = parsedXml.transcript;
return transcript;
}

async function parseXmlString(unparsedXml: string): Promise<any> {
try {
return await parseStringPromise(unparsedXml);
} catch (error) {
throw {
code: 500,
message: 'Could not load the transcript.',
context: error,
};
}
}

function respondWithCaptionTrack(
res: Response,
captionTrack: string
): Response {
return res
.set({ 'Access-Control-Allow-Origin': '*' })
.status(200)
.send(captionTrack);
}

function respondWithError(res: Response, error: any): Response {
res = enableCors(res);
logErrorToConsole(error);

if (error.code) {
return res
.status(error.code)
.json({ error: error.code, error_message: error.message });
}

return res.status(500).json(error);
}

function logErrorToConsole(error: any): void {
console.error(`Error: ${JSON.stringify(error)}`);
}

function enableCors(res: Response): Response {
return res.set({ 'Access-Control-Allow-Origin': '*' });
}
Loading

0 comments on commit 01b92d2

Please sign in to comment.