Skip to content

Commit

Permalink
add vimeo-video-thumbnail endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
robertotcestari committed Sep 12, 2024
1 parent 1ff2849 commit e1b41b0
Show file tree
Hide file tree
Showing 11 changed files with 159 additions and 3,057 deletions.
Binary file modified bun.lockb
Binary file not shown.
2,880 changes: 0 additions & 2,880 deletions package-lock.json

This file was deleted.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"nanoid": "^5.0.7",
"playwright-core": "^1.45.3",
"sharp": "^0.33.4",
"winston": "^3.14.2",
"zod": "^3.23.8"
},
"devDependencies": {
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@ import screenshot from './routes/screenshot';
import uploadImage from './routes/upload-image';
import uploadAvatarImage from './routes/upload-avatar-image';
import avatarPlaceholder from './routes/avatars/avatar-placeholder';
import vimeoVideoThumbnail from './routes/vimeo-video-thumbnail';

const app = new Hono();

// Protected routes
app.use('/screenshot', bearerAuth({ token: process.env.TOKEN! }));
app.use('/upload-image', bearerAuth({ token: process.env.TOKEN! }));
app.use('/upload-avatar-image/*', bearerAuth({ token: process.env.TOKEN! }));
app.use('/vimeo-video-thumbnail', bearerAuth({ token: process.env.TOKEN! }));

app.route('/screenshot', screenshot);
app.route('/upload-image', uploadImage);
app.route('/upload-avatar-image', uploadAvatarImage);
app.route('/vimeo-video-thumbnail', vimeoVideoThumbnail);

// Unprotected routes
app.route('/avatars', avatarPlaceholder);
Expand Down
3 changes: 1 addition & 2 deletions src/lib/hono-validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,7 @@ export function vimeoVideoValidator() {
if (!parsed.success) {
return c.json(
{
message:
'Invalid Data. You need to pass `vimeoUrl`, `localPath`, `position` and `startTime`',
message: 'Invalid Data. You need to pass `vimeoID`',
},
400
);
Expand Down
2 changes: 1 addition & 1 deletion src/lib/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,5 @@ export const screenshotPutRequestSchema = z.object({
});

export const vimeoVideoRequestSchema = z.object({
vimeoUrl: z.string().min(1, 'vimeoUrl is required'),
vimeoID: z.string().min(1, 'vimeoID is required'),
});
68 changes: 0 additions & 68 deletions src/lib/video.ts

This file was deleted.

42 changes: 0 additions & 42 deletions src/lib/vimeo-arraybuf.ts

This file was deleted.

129 changes: 129 additions & 0 deletions src/lib/vimeo-video-thumbnail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import axios, { AxiosError, AxiosResponse } from 'axios';
import Ffmpeg from 'fluent-ffmpeg';
import { S3 } from './s3';
import { nanoid } from 'nanoid';
import { createLogger, transports, format } from 'winston';
import { unlink } from 'node:fs/promises';

// Initialize logger
const logger = createLogger({
level: 'info',
format: format.combine(
format.timestamp(),
format.printf(
({ timestamp, level, message }) => `${timestamp} ${level}: ${message}`
)
),
transports: [new transports.Console()],
});

const vimeoAccessToken: string | undefined = process.env.VIMEO_SECRET;

if (!vimeoAccessToken) {
throw new Error('VIMEO_SECRET environment variable is not set');
}

export async function handle(vimeoID: string): Promise<{ videoUrl: string }> {
const videoAPIUrl: string = `https://api.vimeo.com/videos/${vimeoID}`;

let response: AxiosResponse | null = null;
try {
response = await axios({
url: videoAPIUrl,
method: 'GET',
headers: {
Authorization: `Bearer ${vimeoAccessToken}`,
},
});
} catch (e: any) {
if (e.response?.status === 404) {
throw new Error('Video not found on Vimeo');
}

if (e.response.status !== 200) {
throw new Error('Error fetching video from Vimeo');
}
}

const files: { height: number; link: string }[] = response?.data?.files;

if (!files) {
throw new Error('No files found for this video');
}

const file540p = files.find((file) => file.height === 540);

if (!file540p) {
throw new Error('No 540p file found for this video');
}
const downloadUrl: string = file540p.link;
const downloadResponse = await fetch(downloadUrl);

await Bun.write('tmp/video.mp4', downloadResponse);

logger.info('Video downloaded! Processing video...');

const videoHash = nanoid();
const videoUrl = await processVideo(
'tmp/video.mp4',
'tmp/processed-video.mp4',
async () =>
saveToAWS(
'tmp/processed-video.mp4',
`workshops/cover-videos/${videoHash}.mp4`
)
);

// remove temp files
await unlink('tmp/video.mp4');
await unlink('tmp/processed-video.mp4');

return videoUrl;
}

async function saveToAWS(
videoPath: string,
uploadPath: string
): Promise<string> {
try {
const videoArrBuffer = await Bun.file(videoPath).arrayBuffer();
const videoBuffer = Buffer.from(videoArrBuffer);
logger.info(`Saving to AWS: ${videoPath}`);
const s3 = new S3();
const { videoUrl } = await s3.uploadVideo(uploadPath, videoBuffer);
logger.info('Video saved to AWS!');
logger.info(`Video URL: ${videoUrl}`);
return videoUrl;
} catch (error: any) {
logger.error(`Error saving video to AWS: ${error.message}`);
throw error;
}
}

export async function processVideo(
inputPath: string,
outputPath: string,
callback: () => Promise<string>
): Promise<{ videoUrl: string }> {
logger.info('Processing video...');
logger.info(`Input Path: ${inputPath}`);
logger.info(`Output Path: ${outputPath}`);

return new Promise((resolve, reject) => {
Ffmpeg(inputPath)
.setStartTime('00:00:00')
.setDuration(5)
.noAudio()
.output(outputPath)
.on('end', () => {
callback()
.then((videoUrl) => resolve({ videoUrl }))
.catch((error) => reject(error));
})
.on('error', (err: any) => {
logger.error(`Error processing video: ${err.message}`);
reject(err);
})
.run();
});
}
24 changes: 24 additions & 0 deletions src/routes/vimeo-video-thumbnail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Hono } from 'hono';
import { vimeoVideoValidator } from '../lib/hono-validators';
import { handle } from '../lib/vimeo-video-thumbnail';

const app = new Hono();

app.post('/', vimeoVideoValidator(), async (c) => {
const { vimeoID } = c.req.valid('json');

try {
// Baixar o vídeo do Vimeo
const { videoUrl } = await handle(vimeoID);

return c.json({ message: 'Video downloaded from Vimeo', videoUrl }, 200);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
return c.json(
{ message: `Error downloading the video. ${errorMessage}` },
500
);
}
});

export default app;
64 changes: 0 additions & 64 deletions src/routes/vimeo-video.ts

This file was deleted.

0 comments on commit e1b41b0

Please sign in to comment.