The YouTube API v3 is horribly documented. I've already reported numerous bugs multiple times but no one reacts.
I still have to use this API to upload thumbnails.
The guide states:
POST https://www.googleapis.com/youtube/v3/thumbnails/set
Auth scopes:
- https://www.googleapis.com/auth/youtubepartner
- https://www.googleapis.com/auth/youtube.upload
- https://www.googleapis.com/auth/youtube
Parameters:
- videoId: string The videoId parameter specifies a YouTube video ID for which the custom video thumbnail is being provided.
First of all - the url is wrong. It has to be https://www.googleapis.com/upload/youtube/v3/thumbnails/set
.
Now following code, it uses Unirest
:
final HttpResponse<String> response = Unirest.post("https://www.googleapis.com/upload/youtube/v3/thumbnails/set")
.header("Content-Type", "application/octet-stream")
.header("Authorization", accountService.getAuthentication(account).getHeader())
.field("videoId", videoid)
.field("thumbnail", thumbnail)
.asString();
The received response:
{
"error": {
"errors": [
{
"domain": "global",
"reason": "required",
"message": "Required parameter: videoId",
"locationType": "parameter",
"location": "videoId"
}
],
"code": 400,
"message": "Required parameter: videoId"
}
}
How can this be? The videoId is set!
Anyone already played with this part of the API?
I can change the request to
Unirest.post("https://www.googleapis.com/upload/youtube/v3/thumbnails/set?videoId=" + videoid)
.header("Content-Type", "application/octet-stream")
.header("Authorization", accountService.getAuthentication(account).getHeader())
.field("mediaUpload", thumbnail)
.asString();
This will throw me this error:
{
"error": {
"errors": [
{
"domain": "global",
"reason": "backendError",
"message": "Backend Error"
}
],
"code": 503,
"message": "Backend Error"
}
}
Edit:
Same request with URL posted by Ibrahim Ulukaya (the original url from the reference guide):
{
"error": {
"errors": [
{
"domain": "global",
"reason": "wrongUrlForUpload",
"message": "Uploads must be sent to the upload URL. Re-send this request to https://www.googleapis.com/upload/youtube/v3/thumbnails/set"
}
],
"code": 400,
"message": "Uploads must be sent to the upload URL. Re-send this request to https://www.googleapis.com/upload/youtube/v3/thumbnails/set"
}
}
We dug the issue, here are the steps you have to follow if you don't want to use the library.
1) POST https://www.googleapis.com/upload/youtube/v3/thumbnails/set?videoId=VIDEO_ID&uploadType=resumable
with an empty body
2) get back the URL in the Location: header of the response, and POST to that URL with Content-Type: image/png and the thumbnail in the body
URL will be fixed.
You also need to have specific permission in your channel to set custom thumbnails.
There is PHP and Python examples in our sample code that works.
Here's the Java one, I just wrote and tested, it works.
/*
* Copyright (c) 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.api.services.samples.youtube.cmdline.youtube_cmdline_uploadthumbnail_sample;
import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp;
import com.google.api.client.extensions.java6.auth.oauth2.FileCredentialStore;
import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow;
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
import com.google.api.client.googleapis.json.GoogleJsonResponseException;
import com.google.api.client.googleapis.media.MediaHttpUploader;
import com.google.api.client.googleapis.media.MediaHttpUploaderProgressListener;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.InputStreamContent;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.services.youtube.YouTube;
import com.google.api.services.youtube.YouTube.Thumbnails.Set;
import com.google.api.services.youtube.model.ThumbnailSetResponse;
import com.google.common.collect.Lists;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.List;
/**
* This sample uploads and sets a custom thumbnail for a video by:
*
* 1. Uploading a image utilizing "MediaHttpUploader" 2. Setting the uploaded image as a custom
* thumbnail to the video via "youtube.thumbnails.set" method
*
* @author Ibrahim Ulukaya
*/
public class UploadThumbnail {
/**
* Global instance of the HTTP transport.
*/
private static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport();
/**
* Global instance of the JSON factory.
*/
private static final JsonFactory JSON_FACTORY = new JacksonFactory();
/**
* Global instance of Youtube object to make all API requests.
*/
private static YouTube youtube;
/* Global instance of the format used for the image being uploaded (MIME type). */
private static String IMAGE_FILE_FORMAT = "image/png";
/**
* Authorizes the installed application to access user's protected data.
*
* @param scopes list of scopes needed to run youtube upload.
*/
private static Credential authorize(List<String> scopes) throws IOException {
// Load client secrets.
GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(JSON_FACTORY,
new InputStreamReader(UploadThumbnail.class.getResourceAsStream("/client_secrets.json")));
// Checks that the defaults have been replaced (Default = "Enter X here").
if (clientSecrets.getDetails().getClientId().startsWith("Enter")
|| clientSecrets.getDetails().getClientSecret().startsWith("Enter ")) {
System.out.println(
"Enter Client ID and Secret from https://code.google.com/apis/console/?api=youtube"
+ "into youtube-cmdline-uploadthumbnail-sample/src/main/resources/client_secrets.json");
System.exit(1);
}
// Set up file credential store.
FileCredentialStore credentialStore = new FileCredentialStore(
new File(System.getProperty("user.home"), ".credentials/youtube-api-uploadthumbnail.json"),
JSON_FACTORY);
// Set up authorization code flow.
GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder(
HTTP_TRANSPORT, JSON_FACTORY, clientSecrets, scopes).setCredentialStore(credentialStore)
.build();
// Build the local server and bind it to port 8080
LocalServerReceiver localReceiver = new LocalServerReceiver.Builder().setPort(8080).build();
// Authorize.
return new AuthorizationCodeInstalledApp(flow, localReceiver).authorize("user");
}
/**
* This is a very simple code sample that looks up a user's channel, then features the most
* recently uploaded video in the bottom left hand corner of every single video in the channel.
*
* @param args command line args (not used).
*/
public static void main(String[] args) {
// An OAuth 2 access scope that allows for full read/write access.
List<String> scopes = Lists.newArrayList("https://www.googleapis.com/auth/youtube");
try {
// Authorization.
Credential credential = authorize(scopes);
// YouTube object used to make all API requests.
youtube = new YouTube.Builder(HTTP_TRANSPORT, JSON_FACTORY, credential).setApplicationName(
"youtube-cmdline-addfeaturedvideo-sample").build();
// Get the user selected video Id.
String videoId = getVideoIdFromUser();
System.out.println("You chose " + videoId + " to upload a thumbnail.");
// Get the user selected local image file to upload.
File imageFile = getImageFromUser();
System.out.println("You chose " + imageFile + " to upload.");
InputStreamContent mediaContent = new InputStreamContent(
IMAGE_FILE_FORMAT, new BufferedInputStream(new FileInputStream(imageFile)));
mediaContent.setLength(imageFile.length());
// Create a request to set the selected mediaContent as the thumbnail of the selected video.
Set thumbnailSet = youtube.thumbnails().set(videoId, mediaContent);
// Set the upload type and add event listener.
MediaHttpUploader uploader = thumbnailSet.getMediaHttpUploader();
/*
* Sets whether direct media upload is enabled or disabled. True = whole media content is
* uploaded in a single request. False (default) = resumable media upload protocol to upload
* in data chunks.
*/
uploader.setDirectUploadEnabled(false);
MediaHttpUploaderProgressListener progressListener = new MediaHttpUploaderProgressListener() {
@Override
public void progressChanged(MediaHttpUploader uploader) throws IOException {
switch (uploader.getUploadState()) {
case INITIATION_STARTED:
System.out.println("Initiation Started");
break;
case INITIATION_COMPLETE:
System.out.println("Initiation Completed");
break;
case MEDIA_IN_PROGRESS:
System.out.println("Upload in progress");
System.out.println("Upload percentage: " + uploader.getProgress());
break;
case MEDIA_COMPLETE:
System.out.println("Upload Completed!");
break;
case NOT_STARTED:
System.out.println("Upload Not Started!");
break;
}
}
};
uploader.setProgressListener(progressListener);
// Execute upload and set thumbnail.
ThumbnailSetResponse setResponse = thumbnailSet.execute();
// Print out returned results.
System.out.println("\n================== Uploaded Thumbnail ==================\n");
System.out.println(" - Url: " + setResponse.getItems().get(0).getDefault().getUrl());
} catch (GoogleJsonResponseException e) {
System.err.println("GoogleJsonResponseException code: " + e.getDetails().getCode() + " : "
+ e.getDetails().getMessage());
e.printStackTrace();
} catch (IOException e) {
System.err.println("IOException: " + e.getMessage());
e.printStackTrace();
}
}
/*
* Prompts for a video ID from standard input and returns it.
*/
private static String getVideoIdFromUser() throws IOException {
String title = "";
System.out.print("Please enter a video Id to update: ");
BufferedReader bReader = new BufferedReader(new InputStreamReader(System.in));
title = bReader.readLine();
if (title.length() < 1) {
// If nothing is entered, exits
System.out.print("Video Id can't be empty!");
System.exit(1);
}
return title;
}
/*
* Prompts for the path of the image file to upload from standard input and returns it.
*/
private static File getImageFromUser() throws IOException {
String path = "";
System.out.print("Please enter the path of the image file to upload: ");
BufferedReader bReader = new BufferedReader(new InputStreamReader(System.in));
path = bReader.readLine();
if (path.length() < 1) {
// If nothing is entered, exits
System.out.print("Path can not be empty!");
System.exit(1);
}
return new File(path);
}
}
The answer given is not even close to correct! If you post to this URL without an image in the body:
https://www.googleapis.com/upload/youtube/v3/thumbnails/set?videoId=VIDEO_ID&uploadType=resumable
You will receive this error:
"400: mediaBodyRequired"
This error is described in the YouTube docs at the bottom of this page:
https://developers.google.com/youtube/v3/docs/thumbnails/set
as:
"The request does not include the image content."
Although their documentation leaves a lot of things out (think categoryId), they are dead on here since I just tried the first solution posted by Ibrahim and received the error. Although he stated not to provide the image data in the body, the documentation and my own research show exactly the opposite.
The solution is to post to this URL w/ the image body included. When doing so, I received this response:
{ "kind": "youtube#thumbnailSetResponse", "etag": "\"kYnGHzMaBhcGeLrcKRx6PAIUosY/lcDPfygjJkG-yyzdBp0dKhY2xMY\"", "items": [ { "default": { "url": "//i.ytimg.com/vi/fyBx3v1gmbM/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "//i.ytimg.com/vi/fyBx3v1gmbM/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "//i.ytimg.com/vi/fyBx3v1gmbM/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "//i.ytimg.com/vi/fyBx3v1gmbM/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "//i.ytimg.com/vi/fyBx3v1gmbM/maxresdefault.jpg", "width": 1280, "height": 720 } } ] }
Unfortunately it's not actually changing the thumbnail, but I think that is an issue w/ their API server and the queuing of their thumbnail processing. It does return a successful response though. No idea why the thumbnail doesn't change though.