Load watch progress in one request on dashboard and player
Deploy Vault CMS / deploy (push) Failing after 5s
Deploy Vault CMS / deploy (push) Failing after 5s
Add GET /api/progress/all, which returns the signed-in user's progress and merged watch segments for every video (or one playlist) using two queries. - Dashboard: replaces two sequential requests per video with a single request on load - Player sidebar: replaces one request per playlist video with a single request per playlist Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
// app/api/progress/all/route.ts
|
||||
// Everything the signed-in user has watched, in one response, so pages do not
|
||||
// have to ask about each video separately.
|
||||
import { NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { getViewer } from '@/lib/video-access';
|
||||
import { mergeRanges } from '@/lib/watch-progress';
|
||||
|
||||
type Segment = { startSec: number; endSec: number; watchedAt?: Date };
|
||||
|
||||
/**
|
||||
* GET /api/progress/all – all of the user's progress
|
||||
* GET /api/progress/all?playlistId – only videos in that playlist
|
||||
*
|
||||
* Returns { progress: { [videoId]: { percent, watchedSec, segments } } }.
|
||||
* Videos the user has never played are simply absent.
|
||||
*/
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
const viewer = await getViewer();
|
||||
if (!viewer) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const playlistId = new URL(req.url).searchParams.get('playlistId');
|
||||
const where = {
|
||||
userId: viewer.id,
|
||||
...(playlistId ? { video: { playlistId } } : {}),
|
||||
};
|
||||
|
||||
const [progressRows, segmentRows] = await Promise.all([
|
||||
prisma.videoProgress.findMany({
|
||||
where,
|
||||
select: { videoId: true, percent: true, watchedSec: true },
|
||||
}),
|
||||
prisma.videoWatchSegment.findMany({
|
||||
where,
|
||||
select: { videoId: true, startSec: true, endSec: true, watchedAt: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
const segmentsByVideo = new Map<string, Segment[]>();
|
||||
for (const { videoId, ...segment } of segmentRows) {
|
||||
const list = segmentsByVideo.get(videoId);
|
||||
if (list) list.push(segment);
|
||||
else segmentsByVideo.set(videoId, [segment]);
|
||||
}
|
||||
|
||||
const progress: Record<
|
||||
string,
|
||||
{ percent: number; watchedSec: number; segments: Segment[] }
|
||||
> = {};
|
||||
for (const row of progressRows) {
|
||||
progress[row.videoId] = {
|
||||
percent: row.percent,
|
||||
watchedSec: row.watchedSec,
|
||||
// Older data can hold many overlapping rows per video; send the merged view.
|
||||
segments: mergeRanges(segmentsByVideo.get(row.videoId) ?? []),
|
||||
};
|
||||
}
|
||||
|
||||
return NextResponse.json({ progress });
|
||||
} catch (err: any) {
|
||||
console.error('progress/all GET error', err);
|
||||
return NextResponse.json({ error: err?.message ?? 'server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -173,65 +173,27 @@ export default function DashboardClient() {
|
||||
syncEnrollments();
|
||||
}, [session]);
|
||||
|
||||
// One request for all of this user's progress; videos never played are
|
||||
// simply absent and render as 0%.
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const fetchProgress = async () => {
|
||||
const progressMap: Record<string, VideoProgressData> = {};
|
||||
|
||||
// Fetch progress for playlist videos
|
||||
for (const playlist of playlistsToShow) {
|
||||
for (const video of playlist.videos || []) {
|
||||
try {
|
||||
const [progressRes, segmentsRes] = await Promise.all([
|
||||
fetch(`/api/progress?videoId=${video.id}`),
|
||||
fetch(`/api/progress/segments?videoId=${video.id}`),
|
||||
]);
|
||||
|
||||
if (progressRes.ok) {
|
||||
const progressData = await progressRes.json();
|
||||
const segmentsData = segmentsRes.ok ? await segmentsRes.json() : { segments: [] };
|
||||
progressMap[video.id] = {
|
||||
percent: progressData.percent ?? 0,
|
||||
watchedSec: progressData.watchedSec ?? 0,
|
||||
segments: segmentsData.segments ?? [],
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to fetch progress for video ${video.id}`, err);
|
||||
progressMap[video.id] = { percent: 0, watchedSec: 0, segments: [] };
|
||||
}
|
||||
}
|
||||
try {
|
||||
const res = await fetch('/api/progress/all');
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
if (!cancelled) setVideoProgress(data.progress ?? {});
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch progress', err);
|
||||
}
|
||||
|
||||
// Fetch progress for latest videos
|
||||
for (const video of latestVideos) {
|
||||
try {
|
||||
const [progressRes, segmentsRes] = await Promise.all([
|
||||
fetch(`/api/progress?videoId=${video.id}`),
|
||||
fetch(`/api/progress/segments?videoId=${video.id}`),
|
||||
]);
|
||||
|
||||
if (progressRes.ok) {
|
||||
const progressData = await progressRes.json();
|
||||
const segmentsData = segmentsRes.ok ? await segmentsRes.json() : { segments: [] };
|
||||
progressMap[video.id] = {
|
||||
percent: progressData.percent ?? 0,
|
||||
watchedSec: progressData.watchedSec ?? 0,
|
||||
segments: segmentsData.segments ?? [],
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to fetch progress for video ${video.id}`, err);
|
||||
progressMap[video.id] = { percent: 0, watchedSec: 0, segments: [] };
|
||||
}
|
||||
}
|
||||
|
||||
setVideoProgress(progressMap);
|
||||
};
|
||||
|
||||
if (playlistsToShow.length > 0 || latestVideos.length > 0) {
|
||||
fetchProgress();
|
||||
}
|
||||
}, [playlistsToShow, latestVideos]);
|
||||
fetchProgress();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleOpenVideo = (playlistId: string, videoId: string) => {
|
||||
// navigate to videoplayer page using query params (keeps your current structure)
|
||||
|
||||
+19
-17
@@ -303,32 +303,34 @@ React.useEffect(() => {
|
||||
};
|
||||
}, [videoId]);
|
||||
|
||||
// Fetch segments for all playlist videos
|
||||
// Fetch watched segments for every video in the playlist in one request
|
||||
const loadedPlaylistId: string | undefined = playlist?.id;
|
||||
React.useEffect(() => {
|
||||
if (!playlist?.videos || playlist.videos.length === 0) return;
|
||||
if (!loadedPlaylistId) return;
|
||||
let cancelled = false;
|
||||
|
||||
const fetchPlaylistSegments = async () => {
|
||||
const segments: Record<string, WatchSegment[]> = {};
|
||||
|
||||
for (const video of playlist.videos) {
|
||||
try {
|
||||
const res = await fetch(`/api/progress/segments?videoId=${video.id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
segments[video.id] = data.segments ?? [];
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to fetch segments for video ${video.id}:`, err);
|
||||
try {
|
||||
const res = await fetch(`/api/progress/all?playlistId=${encodeURIComponent(loadedPlaylistId)}`);
|
||||
if (!res.ok || cancelled) return;
|
||||
const data = await res.json();
|
||||
const segments: Record<string, WatchSegment[]> = {};
|
||||
for (const [id, entry] of Object.entries<any>(data.progress ?? {})) {
|
||||
segments[id] = entry.segments ?? [];
|
||||
}
|
||||
// Keep anything newer that arrived from a progress save in the meantime.
|
||||
if (!cancelled) setPlaylistSegments((prev) => ({ ...segments, ...prev }));
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch playlist progress:', err);
|
||||
}
|
||||
|
||||
// Keep anything newer that arrived from a progress save in the meantime.
|
||||
setPlaylistSegments((prev) => ({ ...segments, ...prev }));
|
||||
};
|
||||
|
||||
setPlaylistSegments({});
|
||||
fetchPlaylistSegments();
|
||||
}, [playlist?.videos]);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [loadedPlaylistId]);
|
||||
|
||||
// Fetch user's VideoUnlock records
|
||||
React.useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user