Friday, September 29, 2023

Taming YouTube, Part 2: Replacing OOB OAuth on the Command Line

I want to shell script my way to a better life with YouTube. To do this, I need a shell script which can make authenticated queries against the YouTube API. In the past I've done this by making use of the urn:ietf:wg:oauth:2.0:oob redirect_url. Google has since disabled this method, so now I need to update my shell script to make use of a real redirect_url.

Below are the steps I went through to accomplish this. If you find yourself in need of an oob redirect_url replacement, feel free to grab and use any of the code mentioned below.

Step 1. Install oauth2handler on a server for your choice. I've set it up on an EC2 nano instance over at https://code.benjisimon.com/oauth2handler.

Step 2. Set up a project over at the Google Developer's console. I had a project set up there already, but you can make a new one if you're starting from scratch.

Step 3. Turn on the relevant APIs for your project. I want my shell script to talk to YouTube, so I've enabled the YouTube API.

Step 4. Head over to the credentials section of the console and create a new set of OAuth client ID credentials.

Step 5. Properly configure your credentials. For Application Type, select Web Application, for Name enter the name of your choice. Now for the most delicate setting: adding the correct redirect URL.

If you're using my oauth2handler that URL will be:

  https://domain-hosting-the-script/oauth2handler/<slug>

I'm hosting the oauth2handler over at https://code.benjisimon.com/oauth2handler, and I'm using the slug blog_sample, so my redirect URL is:

 https://code.benjisimon.com/oath2handler/blog_sample

Once all the settings are in place, click Create.

Step 6. Download the JSON credential details.

Step 7. Copy the JSON credentials file onto the server hosting oauth2handler. The name of the file must match the slug that you entered in the redirect_url. oauth2handler expects to find JSON files here:

  /var/www/private/oauth2handler

Continuing my example, I copied the JSON credentials file to:

 /var/www/private/oauth2handler/blog_sample.json
$ cat /var/www/private/oauth2handler/blog_sample.json | jq .
{
  "web": {
    "client_id": "XXXXXXXXXX-idh870p1hr4udihsu9v4s13pc08mm9lo.apps.googleusercontent.com",
    "project_id": "XXXXXXX-XXXX-89019",
    "auth_uri": "https://accounts.google.com/o/oauth2/auth",
    "token_uri": "https://oauth2.googleapis.com/token",
    "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
    "client_secret": "XXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "redirect_uris": [
      "https://code.benjisimon.com/oath2handler/blog_sample"
    ]
  }
}

Step 8. Grab the gapi_auth shell script from Github and set it up to be executable from the command line. You can use gapi_auth and oauth2handler to authenticate any OAuth2 API, not just the YouTube API.

$ gapi_auth -h
Usage: gapi_auth -i CLIENT_ID -p CLIENT_SECRET -r REDIRECT_URI -s scope [-h] [-c context] {init|token}

Step 9. Initialize your shell script for execution by calling gapi_auth with the CLIENT_ID and CLIENT_SECRET mentioned in the JSON credentials file. Set the scopes per the API documentation. My youtubeassist shell script let's you initialize authentication by setting specific variables in ~/.config/youtubeassist/config. Alternatively, you can call gapi_auth directly.

# init with youtubeassist
$ cat ~/.config/youtubeassist/config

# Auth setting
CLIENT_ID=XXXXXXXXXX-idh870p1hr4udihsu9v4s13pc08mm9lo.apps.googleusercontent.com
CLIENT_SECRET=GXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
API_SCOPE="https://www.googleapis.com/auth/youtube"
AUTH_REDIRECT_URI=https://code.benjisimon.com/oauth2handler/youtubeassist
AUTH_TOKEN=`gapi_auth -i $CLIENT_ID -p $CLIENT_SECRET -s "$API_SCOPE" token`

$ youtubeassist -a init
...

# init directly with gapi_auth
$ gapi_auth -i XXXXXXXXXX-idh870p1hr4udihsu9v4s13pc08mm9lo.apps.googleusercontent.com \
            -p GXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX \
            -r https://code.benjisimon.com/oauth2handler/blog_sample \
            -s https://www.googleapis.com/auth/youtube -c blog_sample init
Visit:
https://accounts.google.com/o/oauth2/auth?client_id= XXXXXXXXXX-idh870p1hr4udihsu9v4s13pc08mm9lo.apps.googleusercontent.com&redirect_uri=https://code.benjisimon.com/oauth2handler/blog_sample&scope=https://www.googleapis.com/auth/youtube&response_type=code&access_type=offline&approval_prompt=force
JSON doc?

Step 10. Visit the URL offered by the command line and don't panic when Google gives you an error. In my case, I had a typo in my redirect URL. I fixed the typo in the Google Console, downloaded the credentials again and copied them over to the oauth2handler directory. Hopefully you won't mess up the redirect_url, but as my example shows, there's no harm if you do.

Step 11. Run gapi_auth init and visit the URL offered at the command line. Answer the prompts provided by Google. Paste the resulting JSON document back at the command line and hit Control + d to complete the script.

Step 12. You're done! youtubeassist will run gapi_auth token to get an authentication token whenever it needs one. You can also do this from the command line using gapi_auth directly.

$ token=$(gapi_auth -i XXXXX -p GXXXXX -s https://www.googleapis.com/auth/youtube -c blog_sample token)
$ echo $token
ya29.a0AfB_byBwZPgKpO9d8Whtkp2lZhjCmUF9uep8vqqepuca7SBd44gYoTSo_FzNZk48WxCndq450DcIYDVUVBN2l76cOZ8VNqTFurJmpg6jok5wN3FNHeNIG8AeGmMtoGr-ABtHA9RF1dtBmE1R9ofxIczYSFq_riVxhYw8aCgYKAWASARISFQGOcNnCU1gLb0amDlZ1Sutefe7GGg0171
$ curl 'https://youtube.googleapis.com/youtube/v3/channels?part=statistics&mine=true' \
     --header "Authorization: Bearer $token"  \
     --header 'Accept: application/json'
{
  "kind": "youtube#channelListResponse",
  "etag": "eOJPfNqYIr0W6hNUsh8QW8ETULU",
  "pageInfo": {
    "totalResults": 1,
    "resultsPerPage": 5
  },
  "items": [
    {
      "kind": "youtube#channel",
      "etag": "Gxyk-N0SnISUEq-DoTYlo1umrM0",
      "id": "UCCsd3dwyF1qnzzmk7vrRbbQ",
      "statistics": {
        "viewCount": "13152",
        "subscriberCount": "17",
        "hiddenSubscriberCount": false,
        "videoCount": "37"
      }
    }
  ]
}

The API request above shows that that I've got 37 videos uploaded to YouTube and 17 subscribers. Success! With the authentication complete, it's time to start hacking my way to a more efficient YouTube experience. Stay tuned!

Wednesday, September 27, 2023

Taming YouTube, Part 1: Why?

I have a love-hate relationship with YouTube. On one hand, its endless content is an invaluable resource for learning darn near anything. Where else can you watch a 45 minute video on boiling water, and think, man, that was awesome. Or ponder why does a battleship need to carry hand grenades, or rock out to 59 minutes of near perfect music?

On the other hand, my account is cluttered (hello 600+ items on my 'watch later' list), I find it tricky to track and organize videos--especially the ones I want to come back in the future. And most problematically, the very endless content which I love, can easily result in zombie scrolling. That is, instead of doing something useful with my time, I find myself scrolling page after page of videos.

And don't even get me started on 'YouTube Shorts.' That aspect of YouTube is a disaster. I mean, it's impressive content and kudos to the content creators who make the fascinating work that's featured there. It's just that consuming shorts feels like I'm doing battle with an addictive drug. C'mon man, just one more video, this next one is the best. I promise! Best to just not hit play in the fist place.

To address these issues*, I've conjured up a workflow that lets me be more disciplined about finding, consuming and tracking YouTube content. YouTube offers an API and back in 2017 I wrote a shell script to interact with it. My plan was simple: tweak this shell script to make my workflow trivial to execute. I could then have the good parts of YouTube with the annoyances dialed down.

My plan fell apart almost immediately.

The first step in running the tool I created all those years ago, was to authenticate with Google:

$ youtube_auth init
Visit:
https://accounts.google.com/o/oauth2/auth?client_id=XXXXXXXXXXXXXXXXXXXXXXXXXXX&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=https://www.googleapis.com/auth/youtube&response_type=code
Code?

I dropped this URL into my browser, like I have so many times before. But this time I got an error:

Ahh yes, Google has banned OOB style OAuth. The authorization method that I used back in the day is no longer available. Before I could implement my Life Optimizing YouTube Workflow©, I was going to have to untangle this OAuth challenge. And that, my friends, shall be the topic of part 2 of this series.

*Except YouTube Shorts. That section of YouTube is irredeemable. 

Friday, September 22, 2023

Rocky Mountain National Park - Day 5 - The Last Day

[Composed 7/27/2023]

It's a true testament to the beauty of Estes Park and Colorado in general that on our last day, where we did little more than pack up and drive to the airport, I still managed to catch some amazing landscape and animal photographs. Even from a speeding car, no matter where we looked, there was something gorgeous to see.

And speaking of speeding cars, a word needs to be said about our rental. Over the years, when needed, we've splurged on the extra-large SUV class from various rental car companies. With the massive interior, these are often a joy to be a passenger in (Shira drives & navigates; I cruise direct & code). However, this was the first time we were assigned a Grand Wagoneer. Any car with the name 'wagon' in it is going to conjur up images of the car of my childhood, a Ford Cown Victoria Station Wagon (man, she was beautiful). While practical, these cars were anything but luxury.

The Grand Wagoneer was obviously a high end car, however, it wasn't until I noticed something was a bit strange about the panel above the glove-box in front of my seat that I came to appreciate how high end a car this was. It wasn't just a panel, it was a screen. A bit of poking around, and I found an HDMI input that would let me plug my phone into this screen. Check it out, here's my phone displaying Samsung Dex on a screen directly in front of the front-row passenger, right above the glove box:

Impressive! But why? A few days into our trip, we discovered another hidden screen here:

Again, very impressive, but why?

The Grand Wagoneer also offered a massage feature on the front seats ('rock climber' mode for the win!) and Shira got a kick out of the dashboard indicator that showed how many degrees the car was leaning off center. It's hard to know exactly what model of Grand Wagoneer we were driving--was it the grandest? At a price tag hovering around, or maybe even above, $100,000, this is most likley the fanciest car we've ever driven. One mystery, is that the only indication that the Grand Wagoneer was a Jeep was the tiniest 'Jeep' wording that was printed on the plastic of the tail lights. Who makes the most luxurious vehicle possible and then neglects to paste their brand all over it?

The drive to Denver and flight to DC were uneventful, and our luggage and car was waiting for us at the airport as intended. I simply couldn't have asked for anything more from this adventure: the wildlife, the scenery, the weather, the huffing and puffing at altitude, the lux rental car and even High Beam who kept on messing with me by nibbling on my feet, was all next level. Sure, we gave up sleep and downtime to accomplish all this, but we've got those back in DC. Rocky Mountain National Park, you're a winner.

Rocky Mountain National Park - Day 4 - The Return

[Composed 7/26/2023]

After exploring Sky Pond, we started our descent back to our car. We quickly encountered the notorious waterfall section, and again, I found that only a single boulder gave us any pause. There just wasn't an obvious place for someone of Shira's stature to obviously place her hands and feet to get down. With some assistance from M though, Shira got through and before we knew it we were on the other side of the this tricky obstacle.

At this point, the stars fully aligned and a pika popped out from the rocks, and I was able to snap a number of clear pics. A pika is a small mouse like animal that lives in the harsh ecosystem that exists at high elevations.

Despite their cuddly appearance, American pikas—the smallest members of the lagomorph group—are among North America's toughest animals. Pikas are one of the few mammals in the lower 48 states that can survive their entire lives in alpine terrain, the windswept no-man's-land above tree line.

We made our way back to The Loch, encouraging hikers that we passed that they were just about to their goal. At The Loch, we found a piece of land that jut out into the lake and sat down for a rest and snack. The moutains were in the distance, while trout swam in crystal clear waters at our feet. The scene was perfection.

From The Loch, we continued our descent, eventually seeing some clouds in the distance and hearing some thunder, though the weather remained sunny for us. As we got closer to the parking lot, the crowds increased and we came across families with kids who apparently had skipped the 4am start for a more family friendly afternoon hike.

While the trail stretched on, but we had to be getting close to the and with it lunch and a proper bathroom. I was ready to get off the mountain.

We arrived back at our car at 12:35pm, nearly 8 hours after we started the day. While we were all pretty beat, spirits were high as we'd managed to pull off one of the most legendary hikes in the park. We had lunch at The Wayfinder, a restaurant just outside the park's enterance. The food was good, though the real highlight was the show we could see through the windows. The clouds and thunder we'd heard while hiking finally rolled in, and the afternoon thunderstorms that we'd started our day at 4am to avoid made an appearance. It sure was nice catching the storm indoors, rather than on the trail.

We spent the rest of the day chilling at the Airbnb, starting to pack up. At one point I tried to convince Shira to take a walk through our neighborhood. We made it exactly two minutes and 40 seconds before Shira was like, yeah, no. We're not doing this. We've walked enough for the day.

The hike to Sky Pond is definitely the winner reviews suggest it is. Take your time, and you'll crush it.