Using oauth for a simple command line script to access Google's data

26 January 2015

Deprecation Warning

27 February 2016

I had a need to return to this code and discovered that in the year since I wrote this article there have been significant changes to both the basic ruby client libraries (0.8 to 0.9) and the authorization mechanism. I have not yet had time to figure out the details of these changes, let alone update this article to reflect them. The only thing that's clear to me at the moment is that the documentation has not shown any signs of improvement.

I recently needed to write a simple script to pull some data from a Google website. Since I was grabbing some private data, I needed authorize myself to do that. I found it much more work than I expected, not because it's hard, but because there wasn't much documentation there to guide me - I had to puzzle out what path to go based on lots of not particularly relevant documentation. So once I'd figured it out I decided to write a short account of what I'd done, partly in case I need to do this again, and partly to help anyone else who wants to do this.

First a disclaimer. This is what I figured out, it works for me, at the moment. I haven't done extensive research of whether this is the best way to do what I want (although it sure felt like extensive research while I was doing it). So bear that in mind. (And if you have better ways do let me know.)

I did all of this in Ruby, since that's my familiar scripting language. I also used Google's api library for Ruby. But much of the overall flow would be the same for other languages, so if you're operating outside of Ruby I think much of what I did would still be relevant. I'll try to describe what I'm doing in a language independent view as much as possible, in addition to the ruby examples.

I need to access a private playlist of videos on YouTube and print the titles of the videos on that playlist. [1] Since this is a private playlist, I need to authenticate to Google and set up the necessary authorization for the script so it can get at that private data. I want to run this script without any manual intervention, so I want whatever auth mechanism I use to be something that the script can access itself, at least once I've logged into my laptop.

Before I describe the successful path I followed, I should mention a path I took to a dead end. One of the things that made this simple exercise so tricky is that most of the documentation I read assumed I wanted to write a web-app that was guiding a browser. But I wanted a simple command line app (I guess because I'm old-fashioned that way) that didn't involve a browser. Reading through the Google guide to authentication and authorization I decided to use OAuth 2.0, as that seems to be where Google wants to go. Google then gives several scenarios for OAuth authorization, of which the natural (if complex) one to go for seemed to be Service Accounts. These support server-to-server access with authentication done via public/private key pair. I spent a good bit of time fiddling to get this to work and eventually was able to access google with it successfully, at which point I ran into a wall. With a service account, you effectively create a new user on Google. You then need some mechanism to allow that user to access your personal data. If you are running a domain on Google, there is a way to authorize service accounts to access your domain's data. However I could find no such mechanism for accessing data from a direct google account such as mine. Documentation implied you could do for some properties (such as analytics) but there was no general mechanism, such as one that would work for youtube data. It's always frustrating to spend many hours working out a solution and running into a hard wall like that, if this article does nothing more than save a few people from that effort, then it's worth writing.


Outline flow for authorization

The path that did work for me is based on what Google calls the Installed Application flow, but one that I needed to adapt to ensure I could (mostly) do it without having to manually intervene or use a browser.

To best explain how this works, I'll begin with a simple request to get that youtube listing. Whenever a script makes a request to get google data, you need to include an access token in your request. Google's docs show such an HTTP request like this.

GET /plus/v1/people/me HTTP/1.1
Authorization: Bearer 1/fFBGRNJru1FQd44AzqT3Zg
Host: googleapis.com

The access token is just a random looking bunch of characters. It lasts for a short amount of time, the current documentation says it lasts for just an hour. The access token is what the script needs to do its work, but that just leads to the question - how do you get an access token in the first place?

One way to get an access token is to have a different kind of token - a refresh token. Unlike access tokens, refresh tokens last for a long time. They only expire when they are revoked, they are superseded by later refresh tokens, or when Google has a hissy fit. For our script's purpose a refresh token is just the job. Once I have a refresh token, I can store it in a moderately safe place that the script can get to without manual intervention. I can then access the refresh token when I run the script, and as a first step use the refresh token to get a brand new access token. I can then use the access token for the rest of the script run (providing my script doesn't run longer than the lifetime of an access token - and even Ruby isn't that slow). I don't mind if getting the refresh token involves a manual step, because I don't need to do it very often.

Before I explain how to get the refresh token, there's one other thing about them. Each refresh token (and the access token they obtain) has a limited authorization scope - meaning you say what data they are allowed to access. I can create a refresh token that's only valid for reading my youtube data. If a bad guy were to get this token he could not read my calendar data, nor modify my youtube data. Having different tokens with different scopes helps me limit what I do with each token, which makes me a touch more secure (and less worried with how safely I store the tokens).

To get the refresh token, I do have to get a browser to log into google and authenticate itself as me. Like most people I have browser instances permanently logged into Google on my laptop, so that's no big deal. What I do is go to a google URL that's constructed in such a way to specify the authorization scope that I want. If I do that, while logged into my Google account, google will give me a one-time authorization code. I then take that code and visit another URL and google hands me the refresh token that I want.

Before all of this, there's a further thing I need to do - setup google to use APIs and allow access to the apps I want API access to reach. This is a manual task, but I only need to do it once (unless Google has a really big hissy fit).

So here's the steps I need to go through:

  • Set up Google for API access - a one-time manual action with logged in browser
  • Get a one-time authorization code - needs logged in browser, done rarely
  • Exchange the authorization code for a refresh token - API, done rarely
  • Use the refresh token to get a new access token - api only, done once each time I run the script
  • Use the access token when calling google - api only, done every time I call a google api

Setting up Google

To use APIs with a google account I need to go into Google and set things up. The place I need to be is the Google Developers Console. I already had a project defined in the console, but you'll need to do that if you don't have one already. I then clicked on the project and then the APIs tab (on the left). This shows me of APIs and allows me to enable whichever APIs I wish. I need to ensure the API I want to use is enabled (in this case the Youtube Data API).

Figure 1

I also need to have a OAuth client ID using the "Credentials" tab. I already had one set up.

Figure 2


Getting the one-time authorization code

To get the one-time authorization code you need to hit a specially crafted google URL while logged into Google. Google will then return the authorization code. Google's documentation, and various samples I ran into, explain doing this via a web app. In the course of your normal flow, the web app realizes it needs auth, and sends you over to google.

Google can return the authorization code directly to your web app. All you need to do is run a server on your local machine and tell google its URL - eg localhost:1234. Google will then issue a GET to that URL and include the authorization code as an parameter in the URL. Your code can then easily pick off the parameter. You don't need much of a webserver on this port to pick this up, all it ever needs to do is respond to this one request. This level of simple server doesn't even need Sinatra (Ruby's light weight web server framework), I remember many years ago being in an introductory Ruby class with Prag Dave where we wrote a simple web server in a few minutes. But I was too lazy to do even that.

What I did instead was let my program craft the necessary google URL and print this URL out on the console. I then copy and paste it into my browser. Google (after a little dance to check I know what I'm doing) responds with the authorization code on a web page. I then copy and paste this code back into my script. It's not as smooth as an automated mechanism, but I don't care since I only have to do it once every blue moon.

Let's look at my code for this. I divide any non-trivial command line script into multiple classes, separating the class that handles the command line interaction from an "engine" class that does all the work behind the scenes - essentially a use of Separated Presentation. I do this because I find it easier to separate the command line from the core code when I'm working on them.

Figure 3

Figure 3: Sequence diagram for how my code examples get a refresh token.

For the command-line, I'm using Thor, which is a simple framework for building command-line applications in ruby [2].

require 'thor'
require_relative 'engine'

class CLI < Thor
  include Thor::Actions

  def initialize *args
    super(*args)
    @engine = Engine.new
  end

  desc "auth", "re-authorize the script"
  def auth
    puts "Point browser to following URL:\n\n"
    puts @engine.authorization_url
    puts "\n\n"
    auth_code = ask "paste in the authorization code"
    @engine.renew_refresh_token auth_code
  end
end

CLI.start(ARGV)

Thor maps methods in the CLI class to sub-commands, so if the filename of the script is get-vid I can invoke the authorization logic with get-vid auth. [3]

This authorization logic makes two calls to the underlying engine, one to get the url to display, the second to take the resulting authorization code that I paste in and use it renew the refresh token.

I wrote the engine to handle the logic for futzing with youtube, but much of this code is really about dealing with google authorization. So I separated the authorization code out into a separate object, the GoogleAuthorizer. The engine creates an authorizer on initialization and delegates both the URL and renewal requests to it.

class Engine...

  def initialize
    @auth = GoogleAuthorizer.new(
      token_key: 'api-youtube',
      application_name: 'Gateway Youtube Example',
      application_version: '0.1'
      )
  end

  def authorization_url
    @auth.authorization_url 'https://www.googleapis.com/auth/youtube.readonly'
  end
  
  def renew_refresh_token auth_code
    @auth.renew_refresh_token auth_code
  end

I initialize the authorizer with three bits of data, the application name and version are used in some later API calls, the token key I'll expand on shortly.

class GoogleAuthorizer

  def initialize application_name: nil, application_version: "unknown", token_key: nil
    @application_name = application_name
    @token_store = TokenStore.new(token_key)
    @application_version = application_version
  end

To construct the URL I use ruby's URL manipulation library

class GoogleAuthorizer

  def authorization_url scope
    params = {
      scope: scope,
      redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
      response_type: 'code',
      client_id: @token_store.client_id
    }
    url = {
      host: 'accounts.google.com',
      path: '/o/oauth2/auth',
      query: URI.encode_www_form(params)
    }

    return URI::HTTPS.build(url)
  end

This code constructs a URL that looks something like:

https://accounts.google.com/o/oauth2/auth?
  scope=https://www.googleapis.com/auth/youtube.readonly&
  redirect_uri=urn:ietf:wg:oauth:2.0:oob&
  response_type=code&
  client_id=123456789012.apps.googleusercontent.com

To make it easier to read, I've added newlines and whitespace and decoded the URL escapes. I've also made up the client_id.

The parameters to the URL are:

  • scope: how much api we want to access, in this case we want readonly access to the youtube data api
  • redirect_uri: in the usual flow of using this with a web app, google redirects the browser to another URL (typically a localhost post) and deposits its response there. Using this value tells google I want it displayed in the browser for me to copy and paste
  • response_type: I want a one-time authorization code back
  • client_id I get this from the earlier interaction with the Google Developers Console

Pasting that URL into my browser will (eventually) lead me to a web page from Google that shows the glistening authorization code.


Exchanging the authorization code for a refresh token

Now I have the authorization code I can initiate the second operation, obtaining the refresh token. I do this by contacting the Google authorization resource again, this time supplying the authorization code I just got from them and blending it with my client-secret, a code that identifies me to the google API. I don't need to be logged into Google for this step, nor do I need to use a browser.

At this point I have to face up to another question: where do I store the refresh token once I have it? Since this is a script that I'm the only one using, I could just store it in the source code with something like

def refresh_token
  '1234567890WOxNS_gTztCGW3OBTKcSoKfLXDPc5TA7xz4MEudVrK5jSpoR30zcRFq6'
end

I don't like this as I like to keep my code in repositories which are widely copied and often shared with others. Indeed general security advice is to never keep secrets anywhere inside your repository code tree. It's too easy to accidentally commit a file with a secret, and when done, it's nearly impossible to remove. Since I'm naturally rather careless, I try to arrange things so my inevitable mistakes won't cause lasting damage

Another option is to just dump the token in a file outside the source tree. My hard drive is encrypted, so that's reasonably safe - particularly since all I'm protecting is the dark secrets of my Youtube viewing habits. If I were being a bit more paranoid I could encrypt that file, but then that only raises the question of where to store the encryption key for the file, as I don't want to type in a password every time I use the script.

Since I'm running this on a mac, I decided to use the Mac's built in keychain. This automatically opens when I log in and I can access it with the security command-line application. I'll have to think of something else should I want to run this on my Ubuntu box, but I'll deal with that if I need to do that one day.

Whatever my decision on where to store the refresh token, it is a decision, and one of the signs you need encapsulation is to hide decisions. That's why I created a TokenStore class - to hide the decision of how I store my refresh token. I can also use the same class to store a couple of other little things, such as the client_id that I used earlier. The client_id is something I left in the source code, I'm sure if someone else wants to use this code they can figure out how to take it out.

class TokenStore…

  def client_id
    '123456789012.apps.googleusercontent.com'
  end

To renew the refresh token, I need to use the one-time authorization code I got earlier to request new tokens, dig out the refresh token, and put it into my token store. (I say “tokens”, because Google responds with both an access token and a refresh token.)

class GoogleAuthorizer…

  def renew_refresh_token auth_code
    client = get_client_with_new_tokens(auth_code)
    token = client.authorization.refresh_token
    puts "new token: #{token}"
    @token_store.save_refresh_token token
  end

To request these tokens, I talk again to Google, but this time I find it best to use the ruby client library for the Google api. It took me a bit of time to figure out how to use it, as the documentation is sorely lacking. But it works pretty well once I'd got the hang of it. Here's the code to get the tokens:

class GoogleAuthorizer...

  def get_client_with_new_tokens auth_code
    client = Google::APIClient.new(
      application_name:  @application_name,
      application_version: @application_version
      )

    client.authorization = Signet::OAuth2::Client.new(
      token_credential_uri: 'https://www.googleapis.com/oauth2/v3/token',
      code: auth_code,
      client_id: @token_store.client_id,
      client_secret: @token_store.client_secret,
      redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', 
      grant_type: 'authorization_code'
      )
    client.authorization.fetch_access_token!
    return client
  end

This code first instantiates a google api client object and then gives it an authorization object, which uses the Signet library, which I also found wasn't very well documented. This particular combination of attributes worked for authorizing tokens

  • application_name: not sure how this is used by google, but you need a value here to avoid an error in the client library
  • application_version: similar to the application name. Missing this out didn't get an error for me, but I left it in anyway as the error message for the name told me I needed it.
  • token_credential_uri: the URL to talk to for authorization.
  • code: the one-time authorization code
  • client_id: the client id from the earlier interaction with the Google Developers Console
  • client_secret: You also get this from the earlier interaction with the Google Developers Console. The google documentation says this isn't really a secret for applications like this, but they still call it a secret. I think of it as an enhanced identifier.
  • redirect_uri: I don't know what role this is playing, but when I left it off the library spanked me for a missing grant_type [4]. The google documentation implies I should set it to the same value that I did when requesting the authorization code in the first place.
  • grant_type: tells google I have an authorization code that I'd like to redeem for tokens.

I didn't tap the HTTPS link to Google, but based on the google documentation, the resulting HTTP call should look something like this

POST /oauth2/v3/token HTTP/1.1
Host: www.googleapis.com
Content-Type: application/x-www-form-urlencoded

code=4/v6xr77ewYqhvHSyW6UJ1w7jKwAzu&
client_id=123456789012.apps.googleusercontent.com&
client_secret=ABC1234567890&
redirect_uri=urn:ietf:wg:oauth:2.0:oob&
grant_type=authorization_code

The fetch_access_token! method talks to google to get the tokens. Google sends back some JSON which the client library stores in the Signet authorization object. I can then get at the refresh token and save it in my token store.

class GoogleAuthorizer…

  def renew_refresh_token auth_code
    client = get_client_with_new_tokens(auth_code)
    token = client.authorization.refresh_token
    puts "new token: #{token}"
    @token_store.save_refresh_token token
  end

I can then use the token store to save the token into my Mac's keychain.

class TokenStore…

  def save_refresh_token arg
    cmd = "security add-generic-password -a '#{@keychain_account}' -s '#{@keychain_account}' -w '#{arg}'"
    system cmd
  end

I set the account for the token store to use when I created the engine.

class Engine...

  def initialize
    @auth = GoogleAuthorizer.new(
      token_key: 'api-youtube',
      application_name: 'Gateway Youtube Example',
      application_version: '0.1'
      )
  end

class GoogleAuthorizer...

  def initialize application_name: nil, application_version: "unknown", token_key: nil
    @application_name = application_name
    @token_store = TokenStore.new(token_key)
    @application_version = application_version
  end

class TokenStore…

  def initialize keychain_account
    @keychain_account = keychain_account
  end

So the operating system command that's issued by save_refresh_token is

security add-generic-password -a 'api-youtube' -s 'api-youtube' -w 'ABC123456-other-chars'

The system command needs both a service (-s) and an account (-a) when storing a value. I use the same value for each of them, as I really just want a key-value store.


Using the refresh token to get an access token

The authorization logic is unusual, I expect only to invoke it once every blue moon. Also I now hope that once I've written it, I won't have to futz with it again, and I pulled it out into the authorizer so I can use that class with any other command line script that grabs Google data.

What I do want to do is use all of this setup each time I want to do something useful, or in this case print out the videos on a private playlist. I begin with the Thor CLI

class CLI… (in file list-vid)

  default_task :list
  desc "list", "list items in my playlist"
  def list
    puts @engine.list_playlist
  end

The default_task annotation allows me to run the command list-vid without a sub-command and invoke the list method.

As usual, the CLI just delegates all the work to the engine. The outline of the engine method is pretty simple. First I define the parameters to a request with a simple hash. Next I ask a client object to execute the request, returning some JSON that I parse back into a ruby data structure. Finally I pull what I want out of that data structure to send back to the CLI. At this point, however, I want to keep focus on exchanging the refresh token for an access token. All this work is hidden behind the simple client method call.

class Engine...

  def list_playlist
    request = playlist_request
    response = exec_request(request)
    return response['items'].map{|i| i['snippet']['title']}
  end
  def exec_request requestHash
    JSON.parse(client.execute!(requestHash).body)
  end

The client object is an instance of the Google API client that we saw earlier, although it's configured differently. When executing the particular request, the client object will use an access token, but first it has to use the refresh token to get that access token. I set things up so that the first time the script wants to use a client object, I create one going through the dance with google to get the new access token. Once I've done that I keep this client object in the engine to use again for further requests.

class Engine...

  def client
    @client ||= @auth.api_client
    return @client
  end

All of the business of exchanging refresh tokens for access tokens is handled by the Google client library. I just have to create a Google API object initialized with the right data and get it to fetch an access token.

class GoogleAuthorizer...

  def api_client
   client = Google::APIClient.new(
      application_name: @application_name,
      application_version: @application_version
      )
    
    client.authorization = Signet::OAuth2::Client.new(
      token_credential_uri: 'https://www.googleapis.com/oauth2/v3/token',
      client_id: @token_store.client_id,
      client_secret: @token_store.client_secret,
      refresh_token: @token_store.refresh_token,
      grant_type: 'refresh_token'
      )
    client.authorization.fetch_access_token!
    return client
  end

class TokenStore…

  def client_secret
    @client_secret ||= `security find-generic-password -wa google-client-secret`.chomp
    @client_secret
  end
  def refresh_token
    @refresh_token ||= `security find-generic-password -wa #{@keychain_account}`.chomp
    @refresh_token
  end

I can now use the returned Google API client object for the last step.


Using the access token when calling a Google API

If I use the Google API object to make calls on Google, it ensures that access token is added to the request, as well as organizing the request data in the right way. All I have to do is use execute! method on the client object, passing in a hash with the data for my request.

class Engine...

  def list_playlist
    request = playlist_request
    response = exec_request(request)
    return response['items'].map{|i| i['snippet']['title']}
  end
  def exec_request requestHash
    JSON.parse(client.execute!(requestHash).body)
  end

Since this article is really about using Oauth with Google, I can end it right here. However since I'm also futzing with the Google client library and trying to figure out how to use it to grab some data, I might as well continue with just enough to show how I pull out the titles of the videos in that private playlist.


Getting a list of videos from the Google API

To get data out of youtube (or any Google API) I have to decide which resource I want to talk to, what method I wish to invoke on that resource (they don't talk in terms of HTTP verbs) and use parameters to parameterize the call.

The Google client library provides some affordances for determining the right api_method. I can ask the client object to return me an API object which knows about a particular API.

class Engine...

  def youtube
    client.discovered_api('youtube', 'v3')
  end

This call to discovered_api does two things. Firstly it tells the client object to contact Google and download a description of Google's online API which it then caches. Secondly it returns a new Google API object configured with this information. Since the client caches the API description, I don't have to worry about caching the youtube API object myself. (If I need to talk to more than one Google API, I can use the discovered_apis method, which will get multiple API's data from Google in a single call.)

So what can I do with this Google API object? Sadly it's not immediately obvious. There's only one Google API class, and this one class operates with all the various Google APIs. So if I peruse its documentation (which I had to do from my local gem browser since the online version 404d on me) it doesn't tell me anything about methods to talk to youtube. What it does list are various methods that I can invoke to find out more, so I interrogated it with some ruby calls. First I invoked pry to allow me to interrogate the runtime object.

...

  def list_playlist
    binding.pry
    request = playlist_request
    resp = exec_request(request)
    return resp['items'].map{|i| PlaylistTitle.new(i).title}    
  end

That drops me into the pry REPL. I don't use this much, so I'm not that skilled with it, but I know it allows me to invoke methods on objects.

[1] pry(#<Engine>)> youtube.description
=> "Programmatic access to YouTube features."
[2] pry(#<Engine>)> youtube.discovered_methods
=> []

OK - that's not helpful. But I suspect some clever metaprogramming is going on.

[3] pry(#<Engine>)> youtube.methods
=> [:activities,
 :channel_banners,
 :channel_sections,
 :channels,
 :guide_categories,
 :i18n_languages,
 :i18n_regions,
 :live_broadcasts,
 :live_streams,
 :playlist_items,
 :playlists,
… lots more          

So that's it — the Google API does runtime code generation to add suitable methods to the api object. This way a single api class can have support different APIs, and each API can be generated at run time to be up to date with exactly what Google supports. Should Google change its API at run time, I don't need to update my client library, I'll just get a different set of runtime generated methods — very clever.

If you know me well, you'll know that when I use "clever" to describe some programming, it isn't usually a compliment. The downside of doing runtime code generation is that it's a pain in the neck to find out what methods are available to me. Usually when I want to know what an object does, I can look at some documentation. Even with minimal documentation I can at least see what methods are available. But in this case I can't even do that - I have to futz around with pry just to get a list of available method calls. And these method calls don't come with documentation to explain how to use them.

My hope here is that these runtime-generated methods correspond to the Google API that's described in more generic terms. I see a method here called "playlists" - does this corresponds to the resource in the online API? Thankfully this correspondence holds pretty well. It seems that if I see a Google resource named something like channels I can expect a runtime-generated method on the google api object called channels.

This is all enlightening, but how do I list the contents of a playlist? Each playlist on Google has an ID, so how do I see the what items are on playlist "1234"? Here it's important to understand how the Google youtube data API is organized (other Google APIs may be similar, I haven't looked enough to see). When we talk about an HTTP API using resources, I tend to imagine organizing the API in a URL style similar to that used on the web. So if I want the details about playlist 1234, I would expect a document at a URL that looks something like youtube/api/playlists/1234. I can then acquire that document with an HTTP GET.

But the Youtube Data API isn't structured like this. It has resources, but these resources have their own methods on them, and these methods are some subset of "list", "insert", "update", and "delete". What we have here isn't the usual HTTP verbs of "get", "post", "put" and "delete" but a CRUD style set of verbs [5]. The key thing for me to realize was to think of this API like a set of relational tables.

So to find the contents of a playlist, I have to know that a playlist consists of playlist items, and then formulate the equivalent of select * from playlist_items where playlistId = '1234'

class Engine...

  def playlist_request
    {
      api_method: youtube.playlist_items.list,
      parameters: {
        playlistId: "PLJb2p0qX8R_ojWB5Bx4Q6TzaKcLMvsAim",
        part: 'snippet',
      }
    }    
  end

A second thing to understand in the Google APIs is the notion of parts of a resource. The full data referenced by a google resource like this can be large, so to reduce bandwidth Google breaks up resources into parts and requires me to specify which parts I want. In this case I'm just asking for the snippet part, since that will give the me the video titles that I'm looking for.

class Engine...

  def playlist_request
    {
      api_method: youtube.playlist_items.list,
      parameters: {
        playlistId: "PLJb2p0qX8R_ojWB5Bx4Q6TzaKcLMvsAim",
        part: 'snippet',
      }
    }    
  end

The documentation for playlist items shows me the structure of the playlist items.

When the API client executes the request, it returns a result object which contains lots of details about the interaction. To get to the meat of the result I can invoke the body method on the result object and I'll get back the JSON which I can then parse into a ruby data structure:

class Engine...

  def exec_request requestHash
    JSON.parse(client.execute!(requestHash).body)
  end
pp @engine.exec_request(playlist_request)
# =>
{"kind"=>"youtube#playlistItemListResponse",
 "etag"=>"\"F9iA7pnxqNgrkOutjQAa9F2k8HY/bLvU5-1d6Q4rcW60TlK-JTVNovM\"",
 "pageInfo"=>{"totalResults"=>3, "resultsPerPage"=>5},
 "items"=>
  [{"kind"=>"youtube#playlistItem",
    "etag"=>"\"F9iA7pnxqNgrkOutjQAa9F2k8HY/9bdaIAc39A9cxUknU5cr7i2jzLg\"",
    "id"=>"PL_LBOgO7Mf7ZUfWrfLUl5YJuDy41Yo1j50eP8nIG35Wo",
    "snippet"=>
     {"publishedAt"=>"2014-01-14T23:09:37.000Z",
      "channelId"=>"UC0EbszLD1ceZeAkZ3JEI2GA",
… lots more

The snippet part contains a title element, so I can print out the titles in the returned list by digging a little way into the structure.

class Engine...

  def list_playlist
    request = playlist_request
    response = exec_request(request)
    return response['items'].map{|i| i['snippet']['title']}
  end
  def exec_request requestHash
    JSON.parse(client.execute!(requestHash).body)
  end

Share:
if you found this article useful, please share it. I appreciate the feedback and encouragement

Footnotes

1: This isn't exactly what I was trying to do, but since I'm focusing on the Oauth part of the problem, I've simplified the actual task as much as I can.

2: There are quite a few command-line toolkits in Ruby. I haven't done a proper survey of them, but Thor seems to fit the bill reasonably well for my needs. It does a fair amount of stuff I don't care about, but it keeps that complexity out of the way for the simple things I need.

3: I need to have the get-vid file be executable and put the relevant shebang (#!/usr/bin/env ruby) at the top of the file. I also have to put the command CLI.start(ARGV) at the end to get Thor to start command-line processing.

4: Yes, I think that's an incorrect error message too.

5: C - Create = insert, R - Read = list ("select" in SQL), U - Update = update, and D- Delete = delete.

Significant Revisions

26 January 2015: First published