Best way to upload 30k videos?

127.0.0.1 should be fine, indeed.

Hi @JohnLivingston I got a pb sending data to the upload API with nodejs and the script here, which results in a 400 « Bad request » response:

export default class PeertubeApi {
    static url = "http://127.0.0.1:9001/api/v1/";

    static upload(accessToken, data) {
        const form = new FormData(),
            fileurl = data.paths.wip + data.filename;
        form.append("videofile", fileurl);
        form.append("channelId", 3);
        form.append("name", data.titre);
        fetch(this.url + "videos/upload", {
            method: "POST",
            headers: {
                "Authorization": "Bearer " + accessToken
            },
            body: form
        })
            .then(response => {
                if (response.ok) {
                    Log.debug(response.json());
                } else {
                    Log.debug(response);
                }
            });
    }
}

API works fine, I get client and user tokens, and I can get videos list thru API/videos. I guess authentication is fine too, as I get a 400 « Bad request » error with the header, and a 401 « Unauthorized » without it. So it seems the « body » part is the problem, but I’m not sure what to put here, as the doc only shows examples with curl and not nodejs. Here’s what the FormData object sent as the body looks like:

30/10/2023 10:21:03 [DEBUG]: FormData {
  [Symbol(state)]: [
    {
      name: 'videofile',
      value: '/mnt/c/xampp7215/htdocs/imedia/videos/peertube_migration/wip/aseu_actualite_badminton-20230831.mp4'
    },
    { name: 'channelId', value: '3' },
    { name: 'name', value: 'Tous au volant' }
  ]
}

EDIT: I could edit a video category using API/videos/{id} with same headers, so body is definitely the problem. Also I figured I didn’t have a channelId 3, so used 1 instead, with no success.

The videofile field must contain the file content, not the filepath. In the documentation, this is done using curl with the curl special syntax: --form videofile=@"$FILE_PATH" (the @ tells curl to use the file content).

So, you must read the file content and put it in videofile.
There is an example on this page, that is using a Blob to do so: https://medium.com/deno-the-complete-reference/sending-form-data-using-fetch-in-node-js-8cedd0b2af85

const fileName = "./sample.txt";
const body = new FormData();
const blob = new Blob([await readFile(fileName)]);
body.set("field1", "val1");
body.set("field2", "val2");
body.set("file1", blob, fileName);
1 « J'aime »

Hi again @JohnLivingston, I used new Blob([fs.readFileSync(fileurl)]); instead of new Blob([await readFile(fileurl)]); because it’s in a method and as vsCode says « ‹ await › expressions are only allowed within async functions and at the top levels of modules. ». It works anyway so fine with me, but the thing that eludes me is I can not get this to work for previewfile & thumbnailfile a few lines further in same method. The API doc says it needs a string just like videofile, but it fails with a 400 « Bad request » error if I send a Blob, while the request goes safely if I send a mere string containing the picture url, though obviously it does not upload the preview file. It’s strange that sending a string doesn’t break but doesn’t work neither, while sending the intended Blob makes a bad request. Am I missing something?

According to the documentation, it should work with a Blob. Can you share your code?

Have you checked Peertube’s logs? Maybe the file is invalid (Peertube does some file validation, maybe the file format is not accepted).

1 « J'aime »

Thanks for mentioning the logs, I should check that more often :sweat_smile:

warn[08/11/2023 08:28:07] Incorrect request parameters
{
  "path": "/api/v1/videos/upload",
  "err": {
    "thumbnailfile": {
      "msg": "This thumbnail file is not supported or too large. Please, make sure it is of the following type: .png, .jpg, .jpeg, .webp",
      "param": "thumbnailfile",
      "location": "body"
    },
    "previewfile": {
      "msg": "This preview file is not supported or too large. Please, make sure it is of the following type: .png, .jpg, .jpeg, .webp",
      "param": "previewfile",
      "location": "body"
    }
  }
}

Oops I forget to set the blob type, now that works:

let poster = new Blob([fs.readFileSync(posterurl)], {
  type: "image/jpeg"
});
1 « J'aime »

Nice catch for the blob MIME type!
If you did not add this argument for the video, you should. In case Peertube adds a MIME type test for video in a future release.

Yes I added a type for the video blob too (video/mp4), I figured it was a good habit to keep.

Well that mass upload is a long rocky road, now I’m struggling with captions. Once the video is uploaded and I get its uid, I’m adding a caption as shown here PeerTube
and it looks like everything’s fine: I get a 204 response, the video shows there’s a caption, except it’s like there’s nothing inside, no text shows over the video while playing. But if I delete it and replace with the same file in Peertube backoffice, the captions show as intended. Here’s how I’m doing it:

static videoPostUpload(response, data) {
	response.json()
		.then(result => {
			Log.debug(result);
			data.shortUUID = result.video.shortUUID;
			data.url = `${settings.baseurl}w/${data.shortUUID}`;
			//Add subtitles?
			if (data.subs) {
				Object.keys(data.subs).forEach(lang => {
					let form = new FormData();
					let captionurl = paths.wip + data.subs[lang];
					let caption = new Blob([fs.readFileSync(captionurl)], { type: "application/octet-stream" });
					Log.info(caption);
					form.set("captionfile", caption);

					fetch(`${this.url}videos/${data.shortUUID}/captions/${lang}`, {
						method: "PUT",
						headers: {
							Authorization: `Bearer ${this.accessToken}`
						},
						body: form
					})
						.then(response => {
							if (response.ok) {
								Log.debug("Captions ok");
							}
							Log.debug(response);
						});
				});
			}
		});
}

This method is called once the video is uploaded, with its response and some data sent as arguments. When I run it I get the « Captions ok » log. FWIW I added a mime type here but I first did without it, with no error. The blob I send is the same size as the file so I guess nothing’s wrong here. The url used for fetch looks like http://127.0.0.1:9001/api/v1/videos/fKRrCjVAg1mfLvkoQtGUD9/captions/fr which seems legit. No error in logs neither. I really don’t get it.

In case that helps, here’s the upload video method that calls videoPostUpload if successful:

static videoUpload(data) {
	this.getAccessToken()
		.then((response) => {
			if (response) {
				Log.info(`Importing "${data.filename}"`);

				Mediafiles.moveDatafilesToFolder(data, paths.todo, paths.wip, "debug");
				const form = new FormData(),
					fileurl = paths.wip + data.filename;

				//Get file content.
				const file = new Blob([fs.readFileSync(fileurl)], { type: "video/mp4" });
				form.set("videofile", file, data.filename);

				form.set("channelId", data.channelId);
				form.set("category", data.categoryId);
				form.set("name", data.titre);
				form.set("description", data.description);
				form.set("originallyPublishedAt", data.date);
				form.set("language", data.languageId);
				form.set("privacy", data.privacyId);
				//TODO: playlist
				//Tags
				data.tags.forEach(tag => {
					form.set("tags[]", tag);
				});

				//Poster & thumbnail
				if (data.poster) {
					const posterurl = paths.wip + data.poster;
					const poster = new Blob([fs.readFileSync(posterurl)], { type: "image/jpeg" });
					form.set("previewfile", poster, data.poster);
					form.set("thumbnailfile", poster, data.poster);
				}

				Log.debug(form);

				fetch(this.url + "videos/upload", {
					method: "POST",
					headers: {
						Authorization: `Bearer ${this.accessToken}`
					},
					body: form
				})
					.then(response => {
						Log.debug(response);
						if (response.ok) {
							//Upload done.
							this.videoPostUpload(response, data);
						} else {
							Log.error(response);
						}
					});

			} else {
				Log.fatal("videoUpload: couldn't authorize Peertube API.");
			}
		});
}

Maybe just a browser cache? Try read the video in private mode, after (and only after) having added the caption using the API.

Or an invalid MIME type?
Try text/plain instead of application/octet-stream.
If your are not sure, you can try file -i the_caption_file.srt in your bash console.

I compared the manually uploaded .srt file versus the one done thru the API once they’re changed to .vtt files in /lazy-static/video-captions/, and the 1st one has a « WEBVTT FILE » header while the other one doesn’t. I wonder if that’s not the pb, the API doesn’t add that header when changing srt to vtt files but the BO does. Don’t know how important this header is tho, I wish I could find that file in my Debian but a
find / -type d -name "lazy-static"
shows nothing. I mean, after I opened a bash in my peertube container.
Will try what you suggested, and test uploading vtt file instead of srt too. If you know where that lazy-static folder is, that would help :slight_smile: (I followed your tut about having 2 PT instances in Docker).

For the file -i, i meant on your computer, on the original file.

Looking at the Peertube code, i found a list of MIME types for caption files:

VIDEO_CAPTIONS: {
    MIMETYPE_EXT: {
      'text/vtt': '.vtt',
      'application/x-subrip': '.srt',
      'text/plain': '.srt'
    },
    EXT_MIMETYPE: null as { [ id: string ]: string }
  },

I think it could be it: replace application/octet-stream by text/plain

1 « J'aime »

Well just changing the mime type to plain/text solved the pb, thanks again @JohnLivingston !

cross post :slight_smile:

1 « J'aime »

Hi, thanks for sharing .

If I use this code, I should be able to upload lots of videos in the same time. right?

For now, through reading REST API quick start, I am able to Get user token.

I believe I should use this doc for upload video.

But I don’t know how to use your code. :sweat_smile:

I meant do I need to put all the videos in one folder.
Do I need to replace category or description in your code?

Congrats for solving this.

Hi @Ash3T, yes the code above is only a small part so it’s no surprise you don’t know how to use it, I wouldn’t neither :D. I’d gladly share the whole once it’s done, since it may be useful and imo is part of using free software, but I can’t freely do it for now as I’m paid by a public employer to develop it. I don’t think they’re against this but they must discuss how it’ll be shared (I’d go for github whereas they may think of their own sharing platform), and deciding this may take some time. Plus, it’s not over yet as I’m still working on this and trying to get around some small issues. But quickly yes the idea is you put files (mp4, jpg, srt…) in a todo folder, the script reads a xlsx file to get data (name, description, etc) and the app use those files, data and Peertube API to upload all this. Stay tuned!

2 « J'aime »

That’s great news. Thanks very much :bouquet:. Looking forward to hearing from you soon!

Hi all,

My mass-peertube-upload tool is going fine, in short it reads a xlsx file to get video data like channel, description, tags etc. and folders with mp4 srt and jpeg files to upload all this, then writes peertube url and ids back to the data xlsx. I’ve had some random « EPIPE » errors which I can’t really figure (as mere retries work fine), but tests are globally going nicely. Plus my managers are ok to share this once it’s done :slight_smile:

Still a bit of work to do: besides that random EPIPE error, I need a way to tell whether a file has already been uploaded or not. So far, I simply read the xlsx file to check whether the url field is set, aka the video has already been uploaded. But that’s not 100% reliable. The best way would be to get the filename value that is visible when modifying a video in « advanced parameters » tab. But the API doesn’t send this value. I’ve seen this info in the videoSource table, but I think directly querying the DB is not the best solution. IMO the API should return the original filename. Is there any way to get this info? Can I ask for this value to be returned with the api/v1/videos/{id} query?

1 « J'aime »