As part of a perl project recently I needed to generate a load of videos (using the excellent MLT framework) and upload them to YouTube. As the bulk of the project was already in perl, I needed to write a YouTube upload module for it. Here’s a rough overview of how to authenticate and upload a video (chunked rather than loading it all into memory at once) with perl. Note as well it was quite a struggle to get long-term auth tokens from the Google services – one slight mistake with a parameter and they only give temporary grants which last for an hour or two rather than indefinitely.
package Youtube::Upload; use Moo; use LWP::Authen::OAuth2; use Path::Tiny 'path'; use URI::QueryParam; use JSON::XS qw< encode_json decode_json >; use HTTP::Message; # API described at https://developers.google.com/youtube/v3/docs/videos/update has auth_id => is => 'ro', required => 1; has auth_secret => is => 'ro', required => 1; has redirect_uri => is => 'ro', required => 1; # If you havn't used this for a while, blank these and re-run and you'll # probably need to do some auth against google. has auth_code => is => 'ro'; has auth_token => is => 'ro'; has auth => is => 'lazy', builder => \&_google_auth; sub upload { my ($self, $details, $youtube_code, $video_file) = @_; die "No id to update, but also nothing to upload" if !$youtube_code && !$video_file; my %body = %$details; # Allow all embedding $body{status}{embeddable} = JSON::XS::true; my $magic_split = 'BlA123H123BLAH'; # A unique string... my $is_new = !defined $youtube_code; my ($content, %headers, $uri); if( !$is_new ) { $body{id} = $youtube_code; $content = encode_json(\%body); $headers{'Content-Type'} = 'application/json'; $uri = URI->new( 'https://www.googleapis.com/youtube/v3/videos' ); } else { my $msg = HTTP::Message->new([ 'Content-Type' => 'multipart/related', ]); $msg->add_part( HTTP::Message->new([ 'Content-Type' => 'application/json', ], encode_json(\%body) ) ); my $video_msg = HTTP::Message->new( [ 'Content-Type' => 'video/*', ], $magic_split, ); $msg->add_part( $video_msg ); $content = $msg->as_string; (my $head, $content) = split /\r?\n\r?\n/, $content, 2; my ($k, $v) = split /:\s*/, $head, 2; $headers{$k} = $v; $uri = URI->new( 'https://www.googleapis.com/upload/youtube/v3/videos' ); } delete $body{id}; $uri->query_form_hash({ part => join(',', keys %body), }); my $res; if( $is_new ) { my @content = split /\Q$magic_split/, $content; die "Magic split failed" if @content != 2; my $content_fh = path($video_file)->openr; my $request = HTTP::Request->new( 'POST', $uri, HTTP::Headers->new( %headers ), sub { #warn "chunk uploaded"; return '' if !@content; if( @content > 1 ) { return shift @content; } else { my $read = read $content_fh, my $data, 1_000_000; if( !$read ) { return shift @content; } return $data; } } ); $res = $self->auth->request( $request ); } else { $res = $self->auth->put( $uri, %headers, Content => $content ); } my $cont = $res->decoded_content; my $ret; if( !$res->is_success ) { if($res->code != 403) { # not our video die "Response not success: $cont for " . ( $youtube_code || '' ); } } else { $ret = decode_json $cont; } return ref($ret) ? $ret : {}; } sub _google_auth { my ($self) = @_; my $auth = LWP::Authen::OAuth2->new( service_provider => 'Google', redirect_uri => $self->redirect_uri, client_type => "web server", client_id => $self->auth_id, client_secret => $self->auth_secret, save_tokens => sub { say "Save token string: $_[0]" if !$self->auth_token; }, token_string => $self->auth_token, ); # For debug: #$auth->user_agent->add_handler("request_send", sub { shift->dump(maxlength => 10_000); return }); #$auth->user_agent->add_handler("response_done", sub { shift->dump(maxlength => 10_000); return }); if( !$self->auth_code ) { say $auth->authorization_url( scope=> 'https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.upload', # Need these two to get a refresh token approval_prompt => 'force', access_type => 'offline', ); exit; } $auth->request_tokens( code => $self->auth_code ) if !$self->auth_token; return $auth; }
As per this bug report you need to hack LWP::Authen::OAuth2::AccessToken::Bearer to enable the chunked uploads to work, otherwise it throws an error.
The auth_id
and auth_secret
parameters are given by the google code console when you sign up for YouTube API access, and the redirect_uri
should be where the web app would redirect to after displaying the Google oauth permission grant screen. The first time you run the script, you’ll need to save the auth_code
/ auth_token
parts and use them in future when calling. Even though it should grant a new auth token each time the script is run you still need to present the original one from the look of things.