I tried to setup modal upload video to Youtube using Uppy v5.1.6 and Next.js with Mantine. Uploading the video file is success but i think Uppy might have failed to setup custom metadata file i already made so Youtube made Privacy Status to Public by default, on the payload in console network it looks like this:
------WebKitFormBoundarykzTAZdzxpxr6yNSq
Content-Disposition: form-data; name="metadata"
undefined
------WebKitFormBoundarykzTAZdzxpxr6yNSq
Content-Disposition: form-data; name="video"; filename="test_video.mp4"
Content-Type: video/mp4
and here’s my full code, you can see directly on XHRUpload part inside useEffect:
//* INIT
import { useEffect, useRef, useState } from 'react';
import useGlobalState from '@/state/global'
import { useMutationPost, useListFetcher } from '@/hooks/request';
//* INIT
//* UI
import {
Box,
Modal,
LoadingOverlay
} from '@mantine/core';
import Uppy from '@uppy/core';
import Dashboard from '@uppy/dashboard';
import XHRUpload from '@uppy/xhr-upload';
import 'uppy/dist/uppy.min.css';
import { dateToSystemDate } from '@/lib/helper';
//* UI
export default function CourseVideoModal(props) {
const uppyRef = useRef(null);
const uppyContainerRef = useRef(null);
const [isInitializing, setIsInitializing] = useState(false);
const { selDataView } = useGlobalState();
//* GET Access Token
const { refetch: refetchToken } = useListFetcher(
'/admin/setting_option/google-upload-token',
['google:upload:token'],
{
enabled : false,
retry : false,
staleTime: 0,
cacheTime: 0,
}
);
//* POST Commit Video to DB
const { submitData: commitVideoToDb } = useMutationPost({
endpoint : '/admin/course/video/add',
methodName: 'course:video:add',
successMsg: 'Video added to course',
onSuccess : () => {
props.refetchCourse();
props.handlers.close();
},
});
useEffect(() => {
if (!props.opened || !uppyContainerRef.current) return;
if (uppyRef.current) return;
const initializeUppy = async () => {
setIsInitializing(true);
let accessToken;
try {
const { data } = await refetchToken();
accessToken = data?.accessToken;
if (!accessToken) throw new Error('Access token was empty');
} catch (err) {
console.error('Error fetching upload token:', err);
setIsInitializing(false);
return;
}
const uppy = new Uppy({
restrictions: {
maxFileSize : 5 * 1024 * 1024 * 1024,
allowedFileTypes: ['video/*'],
maxNumberOfFiles: 1,
},
autoProceed: false,
});
uppy.use(Dashboard, {
inline: true,
target: uppyContainerRef.current,
});
uppy.use(XHRUpload, {
id : 'XHRUpload',
endpoint : 'https://www.googleapis.com/upload/youtube/v3/videos?part=snippet,status&uploadType=multipart',
method : 'POST',
formData : true,
fieldName : 'video',
allowedMetaFields: ['metadata'],
headers : {
'Authorization': `Bearer ${accessToken}`
},
setFormData(formData) {
const metadata = {
snippet: {
title : 'Beta Test Video',
description: '',
categoryId : '27',
},
status: { privacyStatus: 'unlisted' },
};
const metadataBlob = new Blob(
[JSON.stringify(metadata)],
{ type: 'application/json' }
);
formData.append('metadata', metadataBlob, 'metadata.json');
return formData;
},
});
uppy.on('upload-success', (file, response) => {
const videoId = response?.body?.id;
if (!videoId) {
uppy.info('No video ID returned from YouTube.', 'error', 5000);
return;
}
commitVideoToDb({
id_course : selDataView.course_id,
youtube_id : videoId,
title : file.meta.title || file.name,
description: file.meta.description || '',
});
});
uppyRef.current = uppy;
setIsInitializing(false);
};
initializeUppy();
return () => {
if (uppyRef.current) {
uppyRef.current.close?.();
uppyRef.current.destroy();
uppyRef.current = null;
}
};
}, [props.opened]);
return (
<Modal
title = "Upload Video"
opened = {props.opened}
onClose = {() => props.handlers.close()}
size = "sm"
centered
keepMounted
>
<LoadingOverlay visible={isInitializing} overlayProps={{ radius: 'sm', blur: 2 }} />
<Box ref={uppyContainerRef} />
</Modal>
);
}
I already solve it using uppy.setMeta
right before XHRUpload initialization
const youtubeMetadata = {
snippet: {
title: 'Beta test video ' + dateToSystemDate(new Date()),
description: '',
categoryId: '27',
},
status: { privacyStatus: 'unlisted' },
};
const metadataFile = new File(
[JSON.stringify(youtubeMetadata)],
'metadata.json',
{ type: 'application/json; charset=UTF-8' }
);
uppy.setMeta({ metadata: metadataFile });
uppy.use(XHRUpload, {
id : 'XHRUpload',
endpoint : 'https://www.googleapis.com/upload/youtube/v3/videos?part=snippet,status&uploadType=multipart',
method : 'POST',
formData : true,
fieldName : 'video',
allowedMetaFields: ['metadata'],
headers : {
'Authorization': `Bearer ${accessToken}`
},
});