Skip to content

Commit

Permalink
Merge pull request code100x#1573 from code100x/feat-appx-video
Browse files Browse the repository at this point in the history
feat: embed appx video player
  • Loading branch information
hkirat authored Nov 21, 2024
2 parents b510fec + 42efa19 commit 315bba1
Show file tree
Hide file tree
Showing 11 changed files with 183 additions and 36 deletions.
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

0 comments on commit 315bba1

Please sign in to comment.