feat: integrate Bunny Stream course videos
This commit is contained in:
@@ -25,6 +25,8 @@ export interface ManagedUserDetail extends ManagedUser {
|
||||
export interface PlatformAnalytics { totalUsers: number; activeUsers: number; publishedCourses: number; completedLessons: number; comments: number; }
|
||||
export interface InstructorAnalytics { courses: number; lessons: number; learners: number; completedLessons: number; comments: number; }
|
||||
export interface AuditEntry { id: string; action: string; subjectType: string; subjectId: string | null; metadata: Record<string, unknown>; createdAt: string; actorName: string; }
|
||||
export interface BunnyVideo { id: string; title: string; status: 'processing' | 'ready' | 'failed'; providerStatus: number; encodeProgress: number; durationSeconds: number | null; }
|
||||
export interface LessonPlayback { provider: string; kind: 'embed' | 'video'; embedUrl?: string; source?: string; expiresAt?: string; }
|
||||
|
||||
interface Session {
|
||||
token: string;
|
||||
@@ -72,6 +74,16 @@ export async function apiRequest<T>(path: string, options: RequestInit = {}): Pr
|
||||
return body as T;
|
||||
}
|
||||
|
||||
async function uploadRequest<T>(path: string, file: File): Promise<T> {
|
||||
const session = getSession();
|
||||
const headers = new Headers({ 'Content-Type': file.type || 'application/octet-stream' });
|
||||
if (session) headers.set('Authorization', `Bearer ${session.token}`);
|
||||
const response = await fetch(`${API_URL}${path}`, { method: 'PUT', headers, body: file });
|
||||
const body = await response.json().catch(() => ({}));
|
||||
if (!response.ok) throw new ApiError(body.error || 'The video upload could not be completed', response.status);
|
||||
return body as T;
|
||||
}
|
||||
|
||||
export const authApi = {
|
||||
async login(email: string, password: string) {
|
||||
return apiRequest<Session>('/auth/login', { method: 'POST', body: JSON.stringify({ email, password }) });
|
||||
@@ -113,4 +125,20 @@ export const adminApi = {
|
||||
|
||||
export const instructorApi = {
|
||||
async analytics() { return apiRequest<{ data: InstructorAnalytics }>('/manage/courses/analytics'); },
|
||||
async bunnyConfiguration() { return apiRequest<{ data: { configured: boolean; maxUploadBytes: number } }>('/manage/media/bunny/config'); },
|
||||
async createBunnyVideo(title: string) {
|
||||
return apiRequest<{ data: BunnyVideo }>('/manage/media/bunny/videos', { method: 'POST', body: JSON.stringify({ title }) });
|
||||
},
|
||||
async uploadBunnyVideo(videoId: string, file: File) {
|
||||
return uploadRequest<{ data: BunnyVideo }>(`/manage/media/bunny/videos/${videoId}/upload`, file);
|
||||
},
|
||||
async bunnyVideoStatus(videoId: string) {
|
||||
return apiRequest<{ data: BunnyVideo }>(`/manage/media/bunny/videos/${videoId}`);
|
||||
},
|
||||
};
|
||||
|
||||
export const courseApi = {
|
||||
async playback(courseId: string, lessonId: string) {
|
||||
return apiRequest<{ data: LessonPlayback }>(`/courses/${courseId}/lessons/${lessonId}/playback`);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Attachment, AttachmentType, Comment, Course, Lesson } from '../types';
|
||||
import { Attachment, AttachmentType, Comment, Course, Lesson, LessonMedia } from '../types';
|
||||
import { apiRequest } from './api';
|
||||
|
||||
export const initDB = async (): Promise<void> => undefined;
|
||||
@@ -27,7 +27,15 @@ type ApiCourse = {
|
||||
description: string;
|
||||
durationSeconds: number | null;
|
||||
accessLevel: 'public' | 'enrolled';
|
||||
media: Array<{ provider: string; playbackUrl: string | null; embedUrl: string | null }>;
|
||||
media: Array<{
|
||||
id: string;
|
||||
provider: string;
|
||||
externalId: string;
|
||||
playbackUrl: string | null;
|
||||
embedUrl: string | null;
|
||||
status: 'processing' | 'ready' | 'failed';
|
||||
durationSeconds: number | null;
|
||||
}>;
|
||||
assets: ApiAsset[];
|
||||
}>;
|
||||
assets: ApiAsset[];
|
||||
@@ -64,16 +72,28 @@ const toCourse = (course: ApiCourse): Course => ({
|
||||
progress: course.progress,
|
||||
lastActivity: course.lastActivity || undefined,
|
||||
duration: `${course.lessons.length} Aulas`,
|
||||
lessons: course.lessons.map((lesson): Lesson => ({
|
||||
id: lesson.id,
|
||||
title: lesson.title,
|
||||
description: lesson.description,
|
||||
duration: toDuration(lesson.durationSeconds),
|
||||
isFree: lesson.accessLevel === 'public',
|
||||
videoUrl: lesson.media[0]?.playbackUrl || lesson.media[0]?.embedUrl || '',
|
||||
mediaProvider: lesson.media[0]?.provider,
|
||||
attachments: lesson.assets.map(toAttachment),
|
||||
})),
|
||||
lessons: course.lessons.map((lesson): Lesson => {
|
||||
const media: LessonMedia[] = lesson.media.map((item) => ({
|
||||
id: item.id,
|
||||
provider: item.provider,
|
||||
externalId: item.externalId,
|
||||
playbackUrl: item.playbackUrl,
|
||||
embedUrl: item.embedUrl,
|
||||
status: item.status,
|
||||
durationSeconds: item.durationSeconds,
|
||||
}));
|
||||
return {
|
||||
id: lesson.id,
|
||||
title: lesson.title,
|
||||
description: lesson.description,
|
||||
duration: toDuration(lesson.durationSeconds),
|
||||
isFree: lesson.accessLevel === 'public',
|
||||
videoUrl: media[0]?.playbackUrl || media[0]?.embedUrl || '',
|
||||
mediaProvider: media[0]?.provider,
|
||||
media,
|
||||
attachments: lesson.assets.map(toAttachment),
|
||||
};
|
||||
}),
|
||||
attachments: course.assets.map(toAttachment),
|
||||
});
|
||||
|
||||
@@ -112,9 +132,18 @@ const toCoursePayload = (course: Course) => ({
|
||||
description: lesson.description || '',
|
||||
durationSeconds: toSeconds(lesson.duration),
|
||||
accessLevel: lesson.isFree ? 'public' as const : 'enrolled' as const,
|
||||
media: lesson.videoUrl && !lesson.videoUrl.startsWith('blob:')
|
||||
? [{ provider: lesson.mediaProvider || 'external', externalId: lesson.videoUrl, playbackUrl: lesson.videoUrl, status: 'ready' as const }]
|
||||
: [],
|
||||
media: lesson.media?.length
|
||||
? lesson.media.map((media) => ({
|
||||
provider: media.provider,
|
||||
externalId: media.externalId,
|
||||
playbackUrl: media.playbackUrl || null,
|
||||
embedUrl: media.embedUrl || null,
|
||||
status: media.status,
|
||||
durationSeconds: media.durationSeconds || null,
|
||||
}))
|
||||
: lesson.videoUrl && !lesson.videoUrl.startsWith('blob:')
|
||||
? [{ provider: lesson.mediaProvider || 'external', externalId: lesson.videoUrl, playbackUrl: lesson.videoUrl, status: 'ready' as const }]
|
||||
: [],
|
||||
assets: (lesson.attachments || []).filter((attachment) => attachment.url && attachment.url !== '#').map(toAssetPayload),
|
||||
})),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user