Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "appxAuthToken" TEXT;

-- AlterTable
ALTER TABLE "VideoMetadata" ADD COLUMN "appxVideoId" TEXT;
36 changes: 18 additions & 18 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -138,29 +139,30 @@ 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[]
videoProgress VideoProgress[]
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 {
Expand Down Expand Up @@ -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
Expand All @@ -359,4 +360,3 @@ enum MigrationStatus {
MIGRATED
MIGRATION_ERROR
}

51 changes: 51 additions & 0 deletions src/actions/user/index.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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<GetAppxAuthTokenResponse> => {
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<string> => {
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;
};
13 changes: 10 additions & 3 deletions src/app/api/admin/content/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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' },
Expand All @@ -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',
},
Expand Down
41 changes: 41 additions & 0 deletions src/components/AppxVideoPlayer.tsx
Original file line number Diff line number Diff line change
@@ -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 <p>Loading...</p>;
}

return <iframe src={url} className="h-[80vh] w-[80vw] rounded-lg"></iframe>;
};
18 changes: 13 additions & 5 deletions src/components/VideoPlayer2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -24,6 +25,7 @@ interface VideoPlayerProps {
onReady?: (player: Player) => void;
subtitles?: string;
contentId: number;
appxVideoId?: string;
onVideoEnd: () => void;
}

Expand All @@ -37,6 +39,7 @@ export const VideoPlayer: FunctionComponent<VideoPlayerProps> = ({
onReady,
subtitles,
onVideoEnd,
appxVideoId,
}) => {
const videoRef = useRef<HTMLDivElement>(null);
const playerRef = useRef<Player | null>(null);
Expand Down Expand Up @@ -311,7 +314,7 @@ export const VideoPlayer: FunctionComponent<VideoPlayerProps> = ({
player.playbackRate(1);
}
};
document.addEventListener('keydown', handleKeyPress, {capture: true});
document.addEventListener('keydown', handleKeyPress, { capture: true });
document.addEventListener('keyup', handleKeyUp);
// Cleanup function
return () => {
Expand Down Expand Up @@ -471,12 +474,17 @@ export const VideoPlayer: FunctionComponent<VideoPlayerProps> = ({
return regex.test(url);
};

if (isYoutubeUrl(vidUrl)) {
return <YoutubeRenderer url={vidUrl} />;
}
if (isYoutubeUrl(vidUrl)) return <YoutubeRenderer url={vidUrl} />;

//TODO: Figure out how to get the courseId
if (appxVideoId)
return <AppxVideoPlayer courseId={'14'} videoId={appxVideoId} />;

return (
<div data-vjs-player style={{ maxWidth: '850px', margin: '0 auto', width: '100%' }}>
<div
data-vjs-player
style={{ maxWidth: '850px', margin: '0 auto', width: '100%' }}
>
<div ref={videoRef} style={{ width: '100%', height: 'auto' }} />
</div>
);
Expand Down
3 changes: 3 additions & 0 deletions src/components/VideoPlayerSegment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ interface VideoProps {
subtitles: string;
videoJsOptions: any;
contentId: number;
appxVideoId?: string;
onVideoEnd: () => void;
}

Expand All @@ -34,6 +35,7 @@ export const VideoPlayerSegment: FunctionComponent<VideoProps> = ({
segments,
videoJsOptions,
onVideoEnd,
appxVideoId,
}) => {
const playerRef = useRef<Player | null>(null);

Expand Down Expand Up @@ -101,6 +103,7 @@ export const VideoPlayerSegment: FunctionComponent<VideoProps> = ({
contentId={contentId}
subtitles={subtitles}
options={videoJsOptions}
appxVideoId={appxVideoId}
onVideoEnd={onVideoEnd}
onReady={handlePlayerReady}
/>
Expand Down
29 changes: 26 additions & 3 deletions src/components/admin/AddContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const AddContent = ({
const [loading, setLoading] = useState<boolean>(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]'
}`;
};
Expand All @@ -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,
Expand Down Expand Up @@ -88,17 +89,21 @@ export const AddContent = ({

return (
<div className="grid grid-cols-1 gap-4 rounded-xl border-2 p-6 lg:grid-cols-7">
<aside className="col-span-1 flex flex-col gap-8 lg:col-span-3">
<aside className="col-span-1 flex w-full flex-col gap-8 lg:col-span-3">
<div>Select the Content Mode</div>

<RadioGroup
className="flex-warp no-scrollbar flex max-w-full items-start gap-4 overflow-auto"
className="flex max-w-full flex-wrap items-start gap-2"
value={type}
onValueChange={(value) => {
setType(value);
setMetadata({});
}}
>
<Label htmlFor="appx" className={getLabelClassName('appx')}>
<RadioGroupItem value="appx" id="appx" />
<span>Appx</span>
</Label>
<Label htmlFor="video" className={getLabelClassName('video')}>
<RadioGroupItem value="video" id="video" />
<span>Video</span>
Expand Down Expand Up @@ -187,6 +192,7 @@ export const AddContent = ({
className="h-14"
/>
{type === 'video' && <AddVideosMetadata onChange={setMetadata} />}
{type === 'appx' && <AddAppxVideoMetadata onChange={setMetadata} />}
{type === 'notion' && <AddNotionMetadata onChange={setMetadata} />}
<Button
onClick={handleContentSubmit}
Expand All @@ -200,6 +206,23 @@ export const AddContent = ({
);
};

function AddAppxVideoMetadata({
onChange,
}: {
onChange: (metadata: any) => void;
}) {
return (
<div>
<Input
type="text"
placeholder="Appx Video Id"
onChange={(e) => onChange({ appxVideoId: e.target.value })}
className="h-14"
/>
</div>
);
}

const VARIANTS = 1;
function AddVideosMetadata({
onChange,
Expand Down
Loading
Loading