107 lines
3.5 KiB
TypeScript
107 lines
3.5 KiB
TypeScript
// app/api/admin/stats/route.ts
|
|
import { NextResponse } from 'next/server';
|
|
import { getServerSession } from 'next-auth';
|
|
import { authOptions } from '@/lib/auth-options';
|
|
import { prisma } from '@/lib/prisma';
|
|
import { Prisma } from '@prisma/client';
|
|
|
|
export async function GET(req: Request) {
|
|
try {
|
|
const session = await getServerSession(authOptions);
|
|
if (!session?.user?.email)
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
|
|
const role = (session as any)?.user?.role ?? null;
|
|
if (!(role === 'admin' || role === 'superadmin')) {
|
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
|
}
|
|
|
|
// Fetch all videos with aggregated stats using raw queries for better performance
|
|
const videos = await prisma.video.findMany({
|
|
include: {
|
|
playlist: {
|
|
select: {
|
|
title: true,
|
|
},
|
|
},
|
|
uploader: {
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
},
|
|
},
|
|
_count: {
|
|
select: {
|
|
likes: true,
|
|
comments: true,
|
|
unlocks: true,
|
|
},
|
|
},
|
|
},
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
|
|
// Fetch detailed stats for each video using raw queries for better performance
|
|
const statsData = await prisma.$queryRaw<
|
|
Array<{
|
|
videoId: string;
|
|
totalViewers: number;
|
|
totalSecondsWatched: bigint;
|
|
avgPercentWatched: number | null;
|
|
totalSegments: number;
|
|
completionCount: number;
|
|
}>
|
|
>`
|
|
SELECT
|
|
p."videoId",
|
|
COUNT(DISTINCT p."userId") as "totalViewers",
|
|
CAST(COALESCE(SUM(p."watchedSec"), 0) AS BIGINT) as "totalSecondsWatched",
|
|
ROUND(CAST(AVG(p."percent") AS NUMERIC), 2) as "avgPercentWatched",
|
|
(SELECT COUNT(*) FROM "VideoWatchSegment" ws WHERE ws."videoId" = p."videoId") as "totalSegments",
|
|
COUNT(CASE WHEN p."completed" = true THEN 1 END) as "completionCount"
|
|
FROM "VideoProgress" p
|
|
GROUP BY p."videoId"
|
|
`;
|
|
|
|
// Create a map for quick lookup
|
|
const statsMap = new Map(statsData.map(item => [item.videoId, item]));
|
|
|
|
// Combine video data with stats
|
|
const videosWithStats = videos.map(video => {
|
|
const stats = statsMap.get(video.id);
|
|
const views = video._count.unlocks || 0;
|
|
const totalViewers = stats ? Number(stats.totalViewers) : 0;
|
|
const completions = stats ? Number(stats.completionCount) : 0;
|
|
const engagement = (video._count.likes || 0) + (video._count.comments || 0);
|
|
const avgWatched = stats?.avgPercentWatched ? Number(stats.avgPercentWatched) : 0;
|
|
|
|
return {
|
|
id: video.id,
|
|
title: video.title,
|
|
playlistTitle: video.playlist.title,
|
|
uploaderName: video.uploader?.name || 'Unknown',
|
|
views,
|
|
totalViewers,
|
|
completions,
|
|
completionRate: totalViewers > 0 ? ((completions / totalViewers) * 100).toFixed(2) : '0.00',
|
|
likes: video._count.likes,
|
|
comments: video._count.comments,
|
|
engagement,
|
|
avgPercentWatched: avgWatched,
|
|
totalSecondsWatched: stats ? Number(stats.totalSecondsWatched) : 0,
|
|
totalSegments: stats ? Number(stats.totalSegments) : 0,
|
|
durationSec: video.durationSec,
|
|
createdAt: video.createdAt,
|
|
};
|
|
});
|
|
|
|
return NextResponse.json(videosWithStats);
|
|
} catch (err: any) {
|
|
console.error('get stats error', err);
|
|
return NextResponse.json(
|
|
{ error: err?.message ?? 'server error' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|