feat: integrate Bunny Stream course videos
All checks were successful
CI / Validate frontend and API (push) Successful in 1m50s
CI / Build and publish Docker images (push) Successful in 18s

This commit is contained in:
Cauê Faleiros
2026-09-02 10:09:36 -03:00
parent 3d4c72e85c
commit 2c137529bc
15 changed files with 607 additions and 40 deletions

View File

@@ -1,12 +1,17 @@
import type { FastifyPluginAsync } from 'fastify';
import { z } from 'zod';
import { pool } from '../db/pool.js';
import { signedBunnyEmbedUrl, BunnyConfigurationError } from '../providers/bunny.js';
const paramsSchema = z.object({
courseId: z.string().uuid(),
});
const playbackParamsSchema = z.object({
courseId: z.string().uuid(),
lessonId: z.string().uuid(),
});
export const courseSelect = (publicOnly = false) => `
export const courseSelect = (publicOnly = false, includeUnreadyMedia = false) => `
select
c.id,
c.slug,
@@ -46,6 +51,7 @@ export const courseSelect = (publicOnly = false) => `
jsonb_build_object(
'id', lm.id,
'provider', lm.provider,
'externalId', lm.external_id,
'status', lm.status,
'embedUrl', lm.embed_url,
'playbackUrl', lm.playback_url,
@@ -53,7 +59,7 @@ export const courseSelect = (publicOnly = false) => `
) order by lm.created_at
)
from lesson_media lm
where lm.lesson_id = l.id and lm.status = 'ready'
where lm.lesson_id = l.id ${includeUnreadyMedia ? '' : "and lm.status = 'ready'"}
), '[]'::jsonb)
) order by l.position
)
@@ -135,4 +141,42 @@ export const courseRoutes: FastifyPluginAsync = async (app) => {
return { data: authenticated ? course : withoutProtectedMedia(course) };
});
app.get('/:courseId/lessons/:lessonId/playback', { preHandler: app.authenticate }, async (request, reply) => {
const { courseId, lessonId } = playbackParamsSchema.parse(request.params);
const result = await pool.query<{
provider: string;
external_id: string;
playback_url: string | null;
embed_url: string | null;
status: string;
}>(
`select lm.provider, lm.external_id, lm.playback_url, lm.embed_url, lm.status
from lesson_media lm
join lessons l on l.id = lm.lesson_id
join courses c on c.id = l.course_id
where c.id = $1 and l.id = $2 and c.status = 'published'
order by lm.created_at
limit 1`,
[courseId, lessonId],
);
const media = result.rows[0];
if (!media) return reply.code(404).send({ error: 'Lesson media was not found' });
if (media.status === 'processing') return reply.code(409).send({ error: 'This video is still being processed by Bunny Stream.' });
if (media.status === 'failed') return reply.code(409).send({ error: 'This video could not be processed. Please contact the instructor.' });
if (media.provider === 'bunny') {
try {
const playback = signedBunnyEmbedUrl(media.external_id);
return { data: { provider: 'bunny', kind: 'embed', ...playback } };
} catch (error) {
if (error instanceof BunnyConfigurationError) return reply.code(503).send({ error: 'Bunny playback is not configured yet.' });
throw error;
}
}
const source = media.playback_url || media.embed_url;
if (!source) return reply.code(409).send({ error: 'This lesson does not have a playable video URL.' });
return { data: { provider: media.provider, kind: 'video', source } };
});
};