Skip to content

Commit

Permalink
feat(Assets): video asset embed code retrieval
Browse files Browse the repository at this point in the history
  • Loading branch information
jakeaturner committed Jun 17, 2024
1 parent 914a460 commit e3e0662
Show file tree
Hide file tree
Showing 9 changed files with 127 additions and 6 deletions.
9 changes: 9 additions & 0 deletions client/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,15 @@ class API {
return res;
}

async getProjectFileEmbedHTML(projectID: string, fileID: string) {
const res = await axios.get<
{
embedHTML: string;
} & ConductorBaseResponse
>(`/project/${projectID}/files/${fileID}/embed`);
return res;
}

public cloudflareStreamUploadURL: string = `${
import.meta.env.MODE === "development" && import.meta.env.VITE_DEV_BASE_URL
}/api/v1/cloudflare/stream-url`;
Expand Down
36 changes: 34 additions & 2 deletions client/src/components/FilesManager/FilesManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import {
import api from "../../api";
import { saveAs } from "file-saver";
import { useTypedSelector } from "../../state/hooks";
import { base64ToBlob } from "../../utils/misc";
import { base64ToBlob, copyToClipboard } from "../../utils/misc";
import { useMediaQuery } from "react-responsive";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";

Expand All @@ -49,6 +49,7 @@ interface FilesManagerProps extends SegmentProps {
toggleFilesManager: () => void;
canViewDetails: boolean;
projectHasDefaultLicense?: boolean;
projectVisibility?: string;
}

type FileEntry = ProjectFile & {
Expand All @@ -63,6 +64,7 @@ const FilesManager: React.FC<FilesManagerProps> = ({
toggleFilesManager,
canViewDetails = false,
projectHasDefaultLicense = false,
projectVisibility = "private",
}) => {
const TABLE_COLS: {
key: string;
Expand Down Expand Up @@ -407,6 +409,23 @@ const FilesManager: React.FC<FilesManagerProps> = ({
}
}

async function handleGetEmbedCode(videoID: string) {
try {
const res = await api.getProjectFileEmbedHTML(projectID, videoID);
if (res.data.err) {
throw new Error(res.data.errMsg);
}
const code = res.data.embedHTML;

await copyToClipboard(
code,
"Embed code copied to clipboard. NOTE: This code is only valid on libretexts.org or libretexts.net domains."
);
} catch (err) {
handleGlobalError(err);
}
}

/**
* Updates state with the a new directory to bring into view.
*
Expand Down Expand Up @@ -481,7 +500,7 @@ const FilesManager: React.FC<FilesManagerProps> = ({
/>
</>
)}
{item.storageType === "file" && (
{item.storageType === "file" && !item.isURL && (
<Dropdown.Item
icon="download"
text="Download"
Expand All @@ -490,6 +509,19 @@ const FilesManager: React.FC<FilesManagerProps> = ({
}
/>
)}
{item.storageType === "file" &&
item.isVideo &&
item.videoStorageID &&
item.access === "public" &&
projectVisibility === "public" && (
<Dropdown.Item
icon="code"
text="Embed"
onClick={async () => {
await handleGetEmbedCode(item.fileID);
}}
/>
)}
</Dropdown.Menu>
</Dropdown>
);
Expand Down
1 change: 1 addition & 0 deletions client/src/components/projects/RenderProjectModules.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ const RenderProjectModules: React.FC<RenderProjectModulesProps> = ({
project.defaultFileLicense &&
Object.keys(project.defaultFileLicense).length > 0
}
projectVisibility={project.visibility}
/>
)}
{!showFiles && (
Expand Down
Empty file.
5 changes: 3 additions & 2 deletions client/src/utils/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,11 @@ export function parseAndFormatDate(date: Date | string, formatString: string) {
return "Unknown Date";
}

export async function copyToClipboard(text: string) {
export async function copyToClipboard(text: string, msg?: string) {
try {
const defaultMsg = "Copied text to clipboard.";
await navigator.clipboard.writeText(text).then(() => {
alert("Copied text to clipboard.");
alert(msg || defaultMsg);
});
} catch (e) {
console.error(e);
Expand Down
5 changes: 5 additions & 0 deletions server/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -1502,6 +1502,11 @@ router.route('/project/:projectID/files/:fileID/captions')
projectfilesAPI.updateProjectFileCaptions,
);

router.route('/project/:projectID/files/:fileID/embed').get(
middleware.validateZod(ProjectFileValidators.getProjectFileEmbedHTMLSchema),
projectfilesAPI.getProjectFileEmbedHTML,
)

router.route('/project/:projectID/book/readerresources')
.get(
authAPI.verifyRequest,
Expand Down
72 changes: 70 additions & 2 deletions server/api/projectfiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import {
videoDataSchema,
updateProjectFileCaptionsSchema,
getProjectFileCaptionsSchema,
getProjectFileEmbedHTMLSchema,
} from "./validators/projectfiles.js";
import { ZodReqWithOptionalUser, ZodReqWithUser } from "../types";
import { ZodReqWithFiles } from "../types/Express";
Expand All @@ -74,6 +75,7 @@ import axios from "axios";
const filesStorage = multer.memoryStorage();
const MAX_UPLOAD_FILES = 20;
const MAX_UPLOAD_FILE_SIZE = 100000000; // 100mb
const LIBRETEXTS_ALLOWED_ORIGINS = ["*.libretexts.org", "*.libretexts.net"];

/**
* Multer handler to process and validate Project File uploads.
Expand Down Expand Up @@ -188,6 +190,7 @@ export async function addProjectFile(
: req.body.videoData;

if (parsedVideoData && parsedVideoData.length) {
const cloudflareUpdates: Promise<any>[] = [];
parsedVideoData.forEach((videoData: z.infer<typeof videoDataSchema>) => {
const newID = v4();
filesToCreate.push({
Expand Down Expand Up @@ -216,8 +219,27 @@ export async function addProjectFile(
videoStorageID: videoData.videoID,
version: 1, // initial version
});

// Set allowedOrigins on Cloudflare Stream
const ENDPOINT = `https://api.cloudflare.com/client/v4/accounts/${process.env.CLOUDFLARE_STREAM_ACCOUNT_ID}/stream/${videoData.videoID}`;
cloudflareUpdates.push(
axios.post(
ENDPOINT,
{
allowedOrigins: LIBRETEXTS_ALLOWED_ORIGINS,
},
{
headers: {
Authorization: `Bearer ${process.env.CLOUDFLARE_STREAM_API_TOKEN}`,
"Content-Type": "application/json",
},
}
)
);
});

await Promise.all(cloudflareUpdates);

await ProjectFile.insertMany(filesToCreate);
filesToCreate.length = 0; // clear array for use by standard files below
}
Expand Down Expand Up @@ -1173,17 +1195,62 @@ async function getProjectFileCaptions(
}
}

async function getProjectFileEmbedHTML(
req: z.infer<typeof getProjectFileEmbedHTMLSchema>,
res: Response
) {
try {
const { projectID, fileID } = req.params;

const file = await ProjectFile.findOne({ projectID, fileID }).lean();
if (!file || !file.videoStorageID) {
return conductor404Err(res);
}

// Check if file is public
if (file.access !== "public") {
return res.status(400).send({
err: true,
errMsg: conductorErrors.err90,
});
}

const ENDPOINT = `https://api.cloudflare.com/client/v4/accounts/${process.env.CLOUDFLARE_STREAM_ACCOUNT_ID}/stream/${file.videoStorageID}/embed`;
const cloudFlareRes = await axios.get(ENDPOINT, {
headers: {
Authorization: `Bearer ${process.env.CLOUDFLARE_STREAM_API_TOKEN}`,
},
});

if (cloudFlareRes.status !== 200) {
throw new Error("Failed to retrieve embed HTML");
}

return res.send({
err: false,
embedHTML: cloudFlareRes.data,
});
} catch (err) {
debugError(err);
return conductor500Err(res);
}
}

async function updateProjectFileCaptions(
req: ZodReqWithFiles<
ZodReqWithUser<z.infer<typeof updateProjectFileCaptionsSchema>>
>,
res: Response
) {
try {
if(!req.files || req.files.length === 0) {
if (!req.files || req.files.length === 0) {
return conductor400Err(res);
}
if(!req.body.language || typeof req.body.language !== "string" || req.body.language.length !== 2) {
if (
!req.body.language ||
typeof req.body.language !== "string" ||
req.body.language.length !== 2
) {
return conductor400Err(res);
}

Expand Down Expand Up @@ -1590,6 +1657,7 @@ export default {
moveProjectFile,
removeProjectFile,
getProjectFileCaptions,
getProjectFileEmbedHTML,
updateProjectFileCaptions,
getPublicProjectFiles,
createProjectFileStreamUploadURL,
Expand Down
4 changes: 4 additions & 0 deletions server/api/validators/projectfiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ export const getProjectFileCaptionsSchema = z.object({
params: _projectFileParams,
});

export const getProjectFileEmbedHTMLSchema = z.object({
params: _projectFileParams,
});

export const getPublicProjectFilesSchema = z.object({
query: PaginationSchema,
});
Expand Down
1 change: 1 addition & 0 deletions server/conductor-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ const conductorErrors = {
"err85": "Unable to create chapter.",
"err86": "Unable to create book. Please check that your book title is unique to its library and try again.",
"err89": "Ticket is not in a valid status for this action.",
"err90": "Cannot get embed code for non-public resource."
};

export default conductorErrors;

0 comments on commit e3e0662

Please sign in to comment.