[SOLVED] Cannot set Metadata when upload video on Youtube API

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 :slight_smile: 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}`
        },
      });