diff --git a/prisma/migrations/20241121062529_add_appx_fields/migration.sql b/prisma/migrations/20241121062529_add_appx_fields/migration.sql new file mode 100644 index 000000000..396bd9092 --- /dev/null +++ b/prisma/migrations/20241121062529_add_appx_fields/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "appxAuthToken" TEXT; + +-- AlterTable +ALTER TABLE "VideoMetadata" ADD COLUMN "appxVideoId" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 66b0c235a..90d2d78a1 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -87,6 +87,7 @@ model NotionMetadata { model VideoMetadata { id Int @id @default(autoincrement()) contentId Int + appxVideoId String? video_1080p_mp4_1 String? // Link to 1080p mp4 quality video variant 1 video_1080p_mp4_2 String? // Link to 1080p mp4 quality video variant 2 video_1080p_mp4_3 String? // Link to 1080p mp4 quality video variant 3 @@ -138,9 +139,9 @@ model Session { } model User { - id String @id @default(cuid()) + id String @id @default(cuid()) name String? - email String? @unique + email String? @unique token String? sessions Session[] purchases UserPurchases[] @@ -148,19 +149,20 @@ model User { comments Comment[] votes Vote[] discordConnect DiscordConnect? - disableDrm Boolean @default(false) - bunnyProxyEnabled Boolean @default(false) + disableDrm Boolean @default(false) + bunnyProxyEnabled Boolean @default(false) bookmarks Bookmark[] password String? appxUserId String? appxUsername String? + appxAuthToken String? questions Question[] answers Answer[] certificate Certificate[] - upiIds UpiId[] @relation("UserUpiIds") - solanaAddresses SolanaAddress[] @relation("UserSolanaAddresses") - githubUser GitHubLink? @relation("UserGithub") - bounties BountySubmission[] + upiIds UpiId[] @relation("UserUpiIds") + solanaAddresses SolanaAddress[] @relation("UserSolanaAddresses") + githubUser GitHubLink? @relation("UserGithub") + bounties BountySubmission[] } model GitHubLink { @@ -324,20 +326,19 @@ model Event { } model BountySubmission { - id String @id @default(uuid()) - prLink String + id String @id @default(uuid()) + prLink String paymentMethod String - status String @default("pending") - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - amount Float @default(0) - userId String - user User @relation(fields: [userId], references: [id]) + status String @default("pending") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + amount Float @default(0) + userId String + user User @relation(fields: [userId], references: [id]) @@unique([userId, prLink]) } - enum VoteType { UPVOTE DOWNVOTE @@ -359,4 +360,3 @@ enum MigrationStatus { MIGRATED MIGRATION_ERROR } - diff --git a/src/actions/user/index.ts b/src/actions/user/index.ts index d85f351e7..7f6f960a7 100644 --- a/src/actions/user/index.ts +++ b/src/actions/user/index.ts @@ -1,5 +1,8 @@ 'use server'; import db from '@/db'; +import { authOptions } from '@/lib/auth'; +import axios from 'axios'; +import { getServerSession } from 'next-auth'; export const logoutUser = async (email: string, adminPassword: string) => { if (adminPassword !== process.env.ADMIN_SECRET) { @@ -25,3 +28,51 @@ export const logoutUser = async (email: string, adminPassword: string) => { return { message: 'User logged out' }; }; + +type GetAppxAuthTokenResponse = { + name: string | null; + email: string | null; + appxAuthToken: string | null; + appxUserId: string | null; +} + +export const GetAppxAuthToken = async (): Promise => { + const session = await getServerSession(authOptions); + if (!session || !session.user) throw new Error("User is not logged in"); + + const user = await db.user.findFirst({ + where: { + email: session.user.email, + }, + select: { + name: true, + email: true, + appxAuthToken: true, + appxUserId: true + } + }); + + if (!user || !user.appxAuthToken) throw new Error("User not found"); + return user; +}; + +export const GetAppxVideoPlayerUrl = async (courseId: string, videoId: string): Promise => { + const { name, email, appxAuthToken, appxUserId } = await GetAppxAuthToken(); + const url = `${process.env.APPX_BASE_API}/get/fetchVideoDetailsById?course_id=${courseId}&video_id=${videoId}&ytflag=${1}&folder_wise_course=${1}`; + + const config = { + url, + method: 'get', + maxBodyLength: Infinity, + headers: { + Authorization: appxAuthToken, + 'Auth-Key': process.env.APPX_AUTH_KEY, + 'User-Id': appxUserId, + }, + }; + + const res = await axios.request(config); + const { video_player_token, video_player_url } = res.data.data; + const full_video_url = `${video_player_url}${video_player_token}&watermark=${name}%0A${email}`; + return full_video_url; +}; diff --git a/src/app/api/admin/content/route.ts b/src/app/api/admin/content/route.ts index 4f3ab38fb..28d57d17b 100644 --- a/src/app/api/admin/content/route.ts +++ b/src/app/api/admin/content/route.ts @@ -48,7 +48,7 @@ export const POST = async (req: NextRequest) => { rest, discordChecked, }: { - type: 'video' | 'folder' | 'notion'; + type: 'video' | 'folder' | 'notion' | 'appx'; thumbnail: string; title: string; courseId: number; @@ -110,6 +110,13 @@ export const POST = async (req: NextRequest) => { }, }); } + } else if (type === 'appx') { + await db.videoMetadata.create({ + data: { + appxVideoId: metadata.appxVideoId, + contentId: content.id, + }, + }); } else if (type === 'video') { await db.videoMetadata.create({ data: { @@ -156,7 +163,7 @@ export const POST = async (req: NextRequest) => { }); } } - if (discordChecked && (type === 'notion' || type === 'video')) { + if (discordChecked && (type === 'notion' || type === 'video' || type === 'appx')) { if (!process.env.NEXT_PUBLIC_DISCORD_WEBHOOK_URL) { return NextResponse.json( { message: 'Environment variable for discord webhook is not set' }, @@ -181,7 +188,7 @@ export const POST = async (req: NextRequest) => { return NextResponse.json( { message: - discordChecked && (type === 'notion' || type === 'video') + discordChecked && (type === 'notion' || type === 'video' || type === 'appx') ? 'Content Added and Discord notification has been sent' : 'Content has been added', }, diff --git a/src/components/AppxVideoPlayer.tsx b/src/components/AppxVideoPlayer.tsx new file mode 100644 index 000000000..573400db7 --- /dev/null +++ b/src/components/AppxVideoPlayer.tsx @@ -0,0 +1,41 @@ +'use client'; +import { GetAppxVideoPlayerUrl } from '@/actions/user'; +import { signOut } from 'next-auth/react'; +import { useEffect, useRef, useState } from 'react'; +import { toast } from 'sonner'; + +export const AppxVideoPlayer = ({ + courseId, + videoId, +}: { + courseId: string; + videoId: string; +}) => { + const [url, setUrl] = useState(''); + const doneRef = useRef(false); + + useEffect(() => { + (async () => { + if (doneRef.current) return; + doneRef.current = true; + try { + const videoUrl = await GetAppxVideoPlayerUrl(courseId, videoId); + setUrl(videoUrl); + } catch { + toast.info('This is a new type of video player', { + description: 'Please relogin to continue', + action: { + label: 'Relogin', + onClick: () => signOut(), + }, + }); + } + })(); + }, []); + + if (!url.length) { + return

Loading...

; + } + + return ; +}; diff --git a/src/components/VideoPlayer2.tsx b/src/components/VideoPlayer2.tsx index 6f72e8dd3..3916917da 100644 --- a/src/components/VideoPlayer2.tsx +++ b/src/components/VideoPlayer2.tsx @@ -16,6 +16,7 @@ import { YoutubeRenderer } from './YoutubeRenderer'; import { toast } from 'sonner'; import { createRoot } from 'react-dom/client'; import { PictureInPicture2 } from 'lucide-react'; +import { AppxVideoPlayer } from './AppxVideoPlayer'; // todo correct types interface VideoPlayerProps { @@ -24,6 +25,7 @@ interface VideoPlayerProps { onReady?: (player: Player) => void; subtitles?: string; contentId: number; + appxVideoId?: string; onVideoEnd: () => void; } @@ -37,6 +39,7 @@ export const VideoPlayer: FunctionComponent = ({ onReady, subtitles, onVideoEnd, + appxVideoId, }) => { const videoRef = useRef(null); const playerRef = useRef(null); @@ -311,7 +314,7 @@ export const VideoPlayer: FunctionComponent = ({ player.playbackRate(1); } }; - document.addEventListener('keydown', handleKeyPress, {capture: true}); + document.addEventListener('keydown', handleKeyPress, { capture: true }); document.addEventListener('keyup', handleKeyUp); // Cleanup function return () => { @@ -471,12 +474,17 @@ export const VideoPlayer: FunctionComponent = ({ return regex.test(url); }; - if (isYoutubeUrl(vidUrl)) { - return ; - } + if (isYoutubeUrl(vidUrl)) return ; + + //TODO: Figure out how to get the courseId + if (appxVideoId) + return ; return ( -
+
); diff --git a/src/components/VideoPlayerSegment.tsx b/src/components/VideoPlayerSegment.tsx index 5c5a9b3fa..f0df83aaa 100644 --- a/src/components/VideoPlayerSegment.tsx +++ b/src/components/VideoPlayerSegment.tsx @@ -24,6 +24,7 @@ interface VideoProps { subtitles: string; videoJsOptions: any; contentId: number; + appxVideoId?: string; onVideoEnd: () => void; } @@ -34,6 +35,7 @@ export const VideoPlayerSegment: FunctionComponent = ({ segments, videoJsOptions, onVideoEnd, + appxVideoId, }) => { const playerRef = useRef(null); @@ -101,6 +103,7 @@ export const VideoPlayerSegment: FunctionComponent = ({ contentId={contentId} subtitles={subtitles} options={videoJsOptions} + appxVideoId={appxVideoId} onVideoEnd={onVideoEnd} onReady={handlePlayerReady} /> diff --git a/src/components/admin/AddContent.tsx b/src/components/admin/AddContent.tsx index 1cdd99174..efe9d9438 100644 --- a/src/components/admin/AddContent.tsx +++ b/src/components/admin/AddContent.tsx @@ -46,7 +46,7 @@ export const AddContent = ({ const [loading, setLoading] = useState(false); const getLabelClassName = (value: string) => { - return `flex gap-6 p-6 rounded-lg items-center space-x-2 ${ + return `flex gap-1 p-4 rounded-lg items-center space-x-2 ${ type === value ? 'border-[3px] border-blue-500' : 'border-[3px]' }`; }; @@ -61,6 +61,7 @@ export const AddContent = ({ title, courseId, parentContentId, + //* Metadata will be list of resolutions for normal videos and appxVideoId for appx videos metadata, adminPassword, courseTitle, @@ -88,17 +89,21 @@ export const AddContent = ({ return (
-