feat: integrate Bunny Stream course videos
This commit is contained in:
@@ -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 } };
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user