Skip to content

Commit 315bba1

Browse files
authored
Merge pull request #1573 from code100x/feat-appx-video
feat: embed appx video player
2 parents b510fec + 42efa19 commit 315bba1

File tree

11 files changed

+183
-36
lines changed

11 files changed

+183
-36
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- AlterTable
2+
ALTER TABLE "User" ADD COLUMN "appxAuthToken" TEXT;
3+
4+
-- AlterTable
5+
ALTER TABLE "VideoMetadata" ADD COLUMN "appxVideoId" TEXT;

prisma/schema.prisma

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ model NotionMetadata {
8787
model VideoMetadata {
8888
id Int @id @default(autoincrement())
8989
contentId Int
90+
appxVideoId String?
9091
video_1080p_mp4_1 String? // Link to 1080p mp4 quality video variant 1
9192
video_1080p_mp4_2 String? // Link to 1080p mp4 quality video variant 2
9293
video_1080p_mp4_3 String? // Link to 1080p mp4 quality video variant 3
@@ -138,29 +139,30 @@ model Session {
138139
}
139140

140141
model User {
141-
id String @id @default(cuid())
142+
id String @id @default(cuid())
142143
name String?
143-
email String? @unique
144+
email String? @unique
144145
token String?
145146
sessions Session[]
146147
purchases UserPurchases[]
147148
videoProgress VideoProgress[]
148149
comments Comment[]
149150
votes Vote[]
150151
discordConnect DiscordConnect?
151-
disableDrm Boolean @default(false)
152-
bunnyProxyEnabled Boolean @default(false)
152+
disableDrm Boolean @default(false)
153+
bunnyProxyEnabled Boolean @default(false)
153154
bookmarks Bookmark[]
154155
password String?
155156
appxUserId String?
156157
appxUsername String?
158+
appxAuthToken String?
157159
questions Question[]
158160
answers Answer[]
159161
certificate Certificate[]
160-
upiIds UpiId[] @relation("UserUpiIds")
161-
solanaAddresses SolanaAddress[] @relation("UserSolanaAddresses")
162-
githubUser GitHubLink? @relation("UserGithub")
163-
bounties BountySubmission[]
162+
upiIds UpiId[] @relation("UserUpiIds")
163+
solanaAddresses SolanaAddress[] @relation("UserSolanaAddresses")
164+
githubUser GitHubLink? @relation("UserGithub")
165+
bounties BountySubmission[]
164166
}
165167

166168
model GitHubLink {
@@ -324,20 +326,19 @@ model Event {
324326
}
325327

326328
model BountySubmission {
327-
id String @id @default(uuid())
328-
prLink String
329+
id String @id @default(uuid())
330+
prLink String
329331
paymentMethod String
330-
status String @default("pending")
331-
createdAt DateTime @default(now())
332-
updatedAt DateTime @updatedAt
333-
amount Float @default(0)
334-
userId String
335-
user User @relation(fields: [userId], references: [id])
332+
status String @default("pending")
333+
createdAt DateTime @default(now())
334+
updatedAt DateTime @updatedAt
335+
amount Float @default(0)
336+
userId String
337+
user User @relation(fields: [userId], references: [id])
336338

337339
@@unique([userId, prLink])
338340
}
339341

340-
341342
enum VoteType {
342343
UPVOTE
343344
DOWNVOTE
@@ -359,4 +360,3 @@ enum MigrationStatus {
359360
MIGRATED
360361
MIGRATION_ERROR
361362
}
362-

src/actions/user/index.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
'use server';
22
import db from '@/db';
3+
import { authOptions } from '@/lib/auth';
4+
import axios from 'axios';
5+
import { getServerSession } from 'next-auth';
36

47
export const logoutUser = async (email: string, adminPassword: string) => {
58
if (adminPassword !== process.env.ADMIN_SECRET) {
@@ -25,3 +28,51 @@ export const logoutUser = async (email: string, adminPassword: string) => {
2528

2629
return { message: 'User logged out' };
2730
};
31+
32+
type GetAppxAuthTokenResponse = {
33+
name: string | null;
34+
email: string | null;
35+
appxAuthToken: string | null;
36+
appxUserId: string | null;
37+
}
38+
39+
export const GetAppxAuthToken = async (): Promise<GetAppxAuthTokenResponse> => {
40+
const session = await getServerSession(authOptions);
41+
if (!session || !session.user) throw new Error("User is not logged in");
42+
43+
const user = await db.user.findFirst({
44+
where: {
45+
email: session.user.email,
46+
},
47+
select: {
48+
name: true,
49+
email: true,
50+
appxAuthToken: true,
51+
appxUserId: true
52+
}
53+
});
54+
55+
if (!user || !user.appxAuthToken) throw new Error("User not found");
56+
return user;
57+
};
58+
59+
export const GetAppxVideoPlayerUrl = async (courseId: string, videoId: string): Promise<string> => {
60+
const { name, email, appxAuthToken, appxUserId } = await GetAppxAuthToken();
61+
const url = `${process.env.APPX_BASE_API}/get/fetchVideoDetailsById?course_id=${courseId}&video_id=${videoId}&ytflag=${1}&folder_wise_course=${1}`;
62+
63+
const config = {
64+
url,
65+
method: 'get',
66+
maxBodyLength: Infinity,
67+
headers: {
68+
Authorization: appxAuthToken,
69+
'Auth-Key': process.env.APPX_AUTH_KEY,
70+
'User-Id': appxUserId,
71+
},
72+
};
73+
74+
const res = await axios.request(config);
75+
const { video_player_token, video_player_url } = res.data.data;
76+
const full_video_url = `${video_player_url}${video_player_token}&watermark=${name}%0A${email}`;
77+
return full_video_url;
78+
};

src/app/api/admin/content/route.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export const POST = async (req: NextRequest) => {
4848
rest,
4949
discordChecked,
5050
}: {
51-
type: 'video' | 'folder' | 'notion';
51+
type: 'video' | 'folder' | 'notion' | 'appx';
5252
thumbnail: string;
5353
title: string;
5454
courseId: number;
@@ -110,6 +110,13 @@ export const POST = async (req: NextRequest) => {
110110
},
111111
});
112112
}
113+
} else if (type === 'appx') {
114+
await db.videoMetadata.create({
115+
data: {
116+
appxVideoId: metadata.appxVideoId,
117+
contentId: content.id,
118+
},
119+
});
113120
} else if (type === 'video') {
114121
await db.videoMetadata.create({
115122
data: {
@@ -156,7 +163,7 @@ export const POST = async (req: NextRequest) => {
156163
});
157164
}
158165
}
159-
if (discordChecked && (type === 'notion' || type === 'video')) {
166+
if (discordChecked && (type === 'notion' || type === 'video' || type === 'appx')) {
160167
if (!process.env.NEXT_PUBLIC_DISCORD_WEBHOOK_URL) {
161168
return NextResponse.json(
162169
{ message: 'Environment variable for discord webhook is not set' },
@@ -181,7 +188,7 @@ export const POST = async (req: NextRequest) => {
181188
return NextResponse.json(
182189
{
183190
message:
184-
discordChecked && (type === 'notion' || type === 'video')
191+
discordChecked && (type === 'notion' || type === 'video' || type === 'appx')
185192
? 'Content Added and Discord notification has been sent'
186193
: 'Content has been added',
187194
},

src/components/AppxVideoPlayer.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
'use client';
2+
import { GetAppxVideoPlayerUrl } from '@/actions/user';
3+
import { signOut } from 'next-auth/react';
4+
import { useEffect, useRef, useState } from 'react';
5+
import { toast } from 'sonner';
6+
7+
export const AppxVideoPlayer = ({
8+
courseId,
9+
videoId,
10+
}: {
11+
courseId: string;
12+
videoId: string;
13+
}) => {
14+
const [url, setUrl] = useState('');
15+
const doneRef = useRef(false);
16+
17+
useEffect(() => {
18+
(async () => {
19+
if (doneRef.current) return;
20+
doneRef.current = true;
21+
try {
22+
const videoUrl = await GetAppxVideoPlayerUrl(courseId, videoId);
23+
setUrl(videoUrl);
24+
} catch {
25+
toast.info('This is a new type of video player', {
26+
description: 'Please relogin to continue',
27+
action: {
28+
label: 'Relogin',
29+
onClick: () => signOut(),
30+
},
31+
});
32+
}
33+
})();
34+
}, []);
35+
36+
if (!url.length) {
37+
return <p>Loading...</p>;
38+
}
39+
40+
return <iframe src={url} className="h-[80vh] w-[80vw] rounded-lg"></iframe>;
41+
};

src/components/VideoPlayer2.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { YoutubeRenderer } from './YoutubeRenderer';
1616
import { toast } from 'sonner';
1717
import { createRoot } from 'react-dom/client';
1818
import { PictureInPicture2 } from 'lucide-react';
19+
import { AppxVideoPlayer } from './AppxVideoPlayer';
1920

2021
// todo correct types
2122
interface VideoPlayerProps {
@@ -24,6 +25,7 @@ interface VideoPlayerProps {
2425
onReady?: (player: Player) => void;
2526
subtitles?: string;
2627
contentId: number;
28+
appxVideoId?: string;
2729
onVideoEnd: () => void;
2830
}
2931

@@ -37,6 +39,7 @@ export const VideoPlayer: FunctionComponent<VideoPlayerProps> = ({
3739
onReady,
3840
subtitles,
3941
onVideoEnd,
42+
appxVideoId,
4043
}) => {
4144
const videoRef = useRef<HTMLDivElement>(null);
4245
const playerRef = useRef<Player | null>(null);
@@ -311,7 +314,7 @@ export const VideoPlayer: FunctionComponent<VideoPlayerProps> = ({
311314
player.playbackRate(1);
312315
}
313316
};
314-
document.addEventListener('keydown', handleKeyPress, {capture: true});
317+
document.addEventListener('keydown', handleKeyPress, { capture: true });
315318
document.addEventListener('keyup', handleKeyUp);
316319
// Cleanup function
317320
return () => {
@@ -471,12 +474,17 @@ export const VideoPlayer: FunctionComponent<VideoPlayerProps> = ({
471474
return regex.test(url);
472475
};
473476

474-
if (isYoutubeUrl(vidUrl)) {
475-
return <YoutubeRenderer url={vidUrl} />;
476-
}
477+
if (isYoutubeUrl(vidUrl)) return <YoutubeRenderer url={vidUrl} />;
478+
479+
//TODO: Figure out how to get the courseId
480+
if (appxVideoId)
481+
return <AppxVideoPlayer courseId={'14'} videoId={appxVideoId} />;
477482

478483
return (
479-
<div data-vjs-player style={{ maxWidth: '850px', margin: '0 auto', width: '100%' }}>
484+
<div
485+
data-vjs-player
486+
style={{ maxWidth: '850px', margin: '0 auto', width: '100%' }}
487+
>
480488
<div ref={videoRef} style={{ width: '100%', height: 'auto' }} />
481489
</div>
482490
);

src/components/VideoPlayerSegment.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ interface VideoProps {
2424
subtitles: string;
2525
videoJsOptions: any;
2626
contentId: number;
27+
appxVideoId?: string;
2728
onVideoEnd: () => void;
2829
}
2930

@@ -34,6 +35,7 @@ export const VideoPlayerSegment: FunctionComponent<VideoProps> = ({
3435
segments,
3536
videoJsOptions,
3637
onVideoEnd,
38+
appxVideoId,
3739
}) => {
3840
const playerRef = useRef<Player | null>(null);
3941

@@ -101,6 +103,7 @@ export const VideoPlayerSegment: FunctionComponent<VideoProps> = ({
101103
contentId={contentId}
102104
subtitles={subtitles}
103105
options={videoJsOptions}
106+
appxVideoId={appxVideoId}
104107
onVideoEnd={onVideoEnd}
105108
onReady={handlePlayerReady}
106109
/>

src/components/admin/AddContent.tsx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export const AddContent = ({
4646
const [loading, setLoading] = useState<boolean>(false);
4747

4848
const getLabelClassName = (value: string) => {
49-
return `flex gap-6 p-6 rounded-lg items-center space-x-2 ${
49+
return `flex gap-1 p-4 rounded-lg items-center space-x-2 ${
5050
type === value ? 'border-[3px] border-blue-500' : 'border-[3px]'
5151
}`;
5252
};
@@ -61,6 +61,7 @@ export const AddContent = ({
6161
title,
6262
courseId,
6363
parentContentId,
64+
//* Metadata will be list of resolutions for normal videos and appxVideoId for appx videos
6465
metadata,
6566
adminPassword,
6667
courseTitle,
@@ -88,17 +89,21 @@ export const AddContent = ({
8889

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

9495
<RadioGroup
95-
className="flex-warp no-scrollbar flex max-w-full items-start gap-4 overflow-auto"
96+
className="flex max-w-full flex-wrap items-start gap-2"
9697
value={type}
9798
onValueChange={(value) => {
9899
setType(value);
99100
setMetadata({});
100101
}}
101102
>
103+
<Label htmlFor="appx" className={getLabelClassName('appx')}>
104+
<RadioGroupItem value="appx" id="appx" />
105+
<span>Appx</span>
106+
</Label>
102107
<Label htmlFor="video" className={getLabelClassName('video')}>
103108
<RadioGroupItem value="video" id="video" />
104109
<span>Video</span>
@@ -187,6 +192,7 @@ export const AddContent = ({
187192
className="h-14"
188193
/>
189194
{type === 'video' && <AddVideosMetadata onChange={setMetadata} />}
195+
{type === 'appx' && <AddAppxVideoMetadata onChange={setMetadata} />}
190196
{type === 'notion' && <AddNotionMetadata onChange={setMetadata} />}
191197
<Button
192198
onClick={handleContentSubmit}
@@ -200,6 +206,23 @@ export const AddContent = ({
200206
);
201207
};
202208

209+
function AddAppxVideoMetadata({
210+
onChange,
211+
}: {
212+
onChange: (metadata: any) => void;
213+
}) {
214+
return (
215+
<div>
216+
<Input
217+
type="text"
218+
placeholder="Appx Video Id"
219+
onChange={(e) => onChange({ appxVideoId: e.target.value })}
220+
className="h-14"
221+
/>
222+
</div>
223+
);
224+
}
225+
203226
const VARIANTS = 1;
204227
function AddVideosMetadata({
205228
onChange,

0 commit comments

Comments
 (0)