Posts by John Tornow

February 12, 2024

Super Bowl

Fantastic Super Bowl game last night. As I suspected, never count out Patrick Mahomes. Just an incredible performance and well deserved title. We had a blast watching this game. The NFL sure does put on a show.

February 11, 2024

Super Bowl Pregame

Super Bowl Sunday. The current line says 49ers by two. After watching him a few weeks ago against the Ravens I’m not going to be betting against Patrick Mahomes any more. I think the Chiefs win this game outright. Maybe when that happens Andy Reid can retire then so the rest of the AFC has a chance next year!

February 10, 2024

Week Notes: February 10, 2024

Happy Saturday from a rainy morning in Texas.

A few links and notes from the week that was:

The Grammy Awards show was on Sunday, and it sure was fun for a change! My kids were interested for only one reason (Taylor) and it didn’t disappoint.

My highlight: a great new single and performance from Billy Joel. It’s not technically his first new song in 30+ years, but it feels like it. The song is great. The chord structures and vocal lines are vintage Joel and sound like they were written decades ago in his songwriting prime. So much fun.

The Apple Vision Pro is in the hands of customers and it’s been a fun week watching videos and reviews from the early adopters. Casey Neistat’s video of wearing the device skateboarding around New York is brilliant. Love seeing the reactions of people around the city.

Nick Bilton had a solid interview with Tim Cook in Vanity Fair. Ben Thompson was underwhelmed by the productivity solution for work.1

I saw a few people wearing the Vision Pro around town this week. They all looked ridiculous and so nerdy, but I suspect this will be more normalized in the coming years.

As with most years, I’ve spent a ton of time in the early part of this year planning features, projects, and other timelines for my teams. In years past I’ve just used Google Sheets as a visual means to display this information.

This year I set everything up in Notion and it’s been such an easier process. Dragging project entries and creating a database of our yearly plans has been a significant improvement over a spreadsheet!

It’s been a few years since I switched all of my ventures to use Notion, and I’m really glad I did.

This week Bluesky opened up officially for all new users, no invite codes necessary.

I signed up for Bluesky a while back but never stuck with it. I don’t have a particularly good reason why, but it just never clicked with me yet. Mastodon and Threads have completely replaced Twitter for me, and I’m not sure I need a third service to check. But I do love the idea of the AT Protocol and the ideas behind Bluesky.

The developer documentation they’ve produced thus far has been very good. I love the spirit behind the protocol and how the company is pushing it. It may be time to give Bluesky another look.

Disney has made a major investment in Epic Games, creator of Fortnite. This is a very interesting deal to me. It’s been a few years since the metaverse craze and things have mostly been quiet on that front for me.

Fortnite is a massive deal. My son and almost all of his friends play the game every chance they get. When they’re not playing it, they’re watching YouTube videos about it. There’s a giant community of gamers that are very into this ecosystem.

It makes a ton of sense to me for the Disney brand to invest in this space and reach this audience. Can’t wait to see how this ends up taking shape.

In a book that was released what seems like a few years too late, Chris Dixon’s Read Write Own is out. Although after reading Molly White’s detailed review it doesn’t seem like it’s worth the time.

Dixon had some really interesting ideas that resonated with me a few years ago. But after so many cases of fraud, deception, and unethical behavior it’s hard for me to take anything crypto seriously anymore. Probably a shame, because there’s some very interesting tech in the space, but it hasn’t resulted in any life changing products for me.

Lastly, in today’s issue of Air Mail, Brian Stelter writes about the rise and fall of the Messenger. A great read about a very weird story. I really like Stetler and his work over the years. It’s so great to having him writing for us.

📰

I disagree with Ben, by the way. More to come on that eventually.

February 9, 2024

Lamar Jackson Wins Second MVP

Congratulations to Lamar Jackson for winning his second NFL MVP award. Lamar is such a joy to watch and the team really is on his back most of the year.

The Ravens are custom-built around his particular skillset and play design. He’s the single most important player on the team and I don’t see anyone else around the league that is relied on more than Lamar. Well deserved.

February 3, 2024

Week Notes: February 3, 2024

Happy Saturday. It was a very busy week. As is now my weekly routine, here are a few notes from the work week that was …

It’s Apple Vision Pro launch week. My pre-order arrived yesterday and I’ve been so busy I haven’t played with it yet. Hoping to find some time this weekend to dig in and figure this thing out.

I officially kicked off a new venture this week. No name or details to share just yet. It’s so nice to start fresh sometimes, and I’m really excited about this one. One of the challenges ahead is developing a design system and tech approach for rapid prototyping and sharing logic between a suite of apps. My goal is to make new ideas and new prototypes simple and quick. More to come here, for sure.

Google has notified users that they will be deleting all data from Universal Analytics (the old GA) in July. We have a ton of good data in our Air Mail accounts. I’m still annoyed we had to make the switch to GA4. It has been an inferior product in every way for us, and I suspect many other publishers too. Time to begin getting our data out of UA and into a proper warehouse.

Speaking of GA4, one feature I do really like is using Looker Studio for some internal dashboards and reports. Our audience team is doing some incredible things here and it’s a very nice tool. We’ve also integrated it with some of our archive and usage data in BigQuery and it’s a breeze to write queries and reports for the team to consume.

This week we also began the process of upgrading Air Mail to the latest versions of Ruby, and Rails. We’re not very far behind, but even a version or two can be complicated for an app of this size and scale. Upgrading Ruby was a piece of cake and took less than an hour. The Rails upgrade on the other hand, is a work in process.. This undertaking is still so much easier than it was a decade ago.

I’m also working on moving us off of CircleCI for our continuous integration. We’ve really had no issues with CircleCI, but our Github account offers most of the same features for ‘free’ with our existing paid team account. Github Actions is not very easy to use, but it seems good enough for our purposes.

Developing for Github Actions is a giant mess of trial and error commits. It took us about 40 commits to get the config where we wanted it for our needs. We tried a few of the tools out there to test the scripts and run locally, but none of them did the trick accurately.

This week’s Air Mail issue is rolling out a new test feature for a small subset of readers: AI translated text. We’re experimenting with letting readers consume a few articles in other languages. The AI process for converting and updating text is quite incredible. Luckily we have many non-english speaking staff members, so there’s plenty of folks to read through the pieces in other languages. The tech here is not quite ready for us to publish without a quick staff edit, but it’s getting close. Fun tech to play with.

Speaking of AI, we’re actively exploring and integrating with Microsoft’s OpenAI integrations through Azure. There was a quick application process to get in, and it took us less than a day to get approved. The tools Azure provides are very nice, and seem to be much closer than giving us what we need. I’m hopeful there’s more to come here.

Registering a domain this week was more complicated than ever before. Apparently now we need to ‘verify’ domain details after purchase through an email and from process. Weird! I used to just purchase domains and use them within an hour with no fuss. Now I’m waiting for Hover to get its act together and send me the proper link to verify the domain details, as its automated initial email contains a broken link. Ouch.

Lastly, I finally upgraded to a paid plan on Casey Newton’s excellent Platformer. I should have done this months ago.

🤖

February 2, 2024

Lewis Hamilton to Ferrari

Major F1 news came down yesterday: Lewis Hamilton is joining Ferrari next year.

Luke Smith, writing at The Athletic:

It’s the kind of move F1 fans — and the figures at the top of the
sport itself — could have only dreamed of ever happening. Partnering
Hamilton, F1’s most famous and successful driver, with Ferrari, F1’s
most famous and successful team, is box office stuff.
Ferrari will likely enter the 2025 season with the strongest lineup in
F1 as Hamilton races alongside Charles Leclerc, its young star. As
‘superteam’ lineups go, short of the implausible prospect of Hamilton
teaming up with Max Verstappen, it’s hard to think of any bigger.
Regardless of the outcome, this will be one of the defining stories in
F1 for the next couple of years as the 39-year-old Hamilton bids to
write the latest — and potentially final — chapter of his glittering
F1 career in Ferrari’s famous red cars.

This was a complete shock. There’s certainly been rumors for years, but I never expected this to be a reality. How could Mercedes let this happen? Wow.

February 1, 2024

Now Playing: A Web Component

One of the aspects of personal websites that I love is the ability to have a little bit of creative expression and "features" that are just for fun. Case in point here: the ability to see what I'm currently listening to on Spotify. Just for kicks I put this quick component together to do just that. This isn't very complicated at all, and would be simple to set up on your own site if you're so inclined.

Here's what it looks like:

If you're seeing an album cover and song name, it's because I'm listening to music right now at this very moment! Neat. If you see a "Not Playing" label, it's because I'm not listening currently. If now is one of those silent moments, here's an image of what it looks like:

Example of the Now Playing component when a track is playing in Spotify.
Example of the component when a track is playing in Spotify.

The song's name, album, artist, link, and artwork all are populated from Spotify. I'm using the Get Playback State endpoint to get these details. (You'll also need a developer account and an app created on Spotify to do this.)

The Code

Here's the exact code that I used to build the first version of this script. It may change over time, but these are the basics:

Web Component Markup

Inside of your HTML, place the markup for the component itself. I'm using Tailwind CSS for the styling, but you can certainly add your own.

There are templating slots for each of the data points I'm using here (artist, track name, artwork, etc). I'm using slots here to avoid specific class name selectors in the JavaScript below.

In the example above, I'm also using some placeholder markup and a fallback image to display when there's no active track playing. I didn't include that in the code below for simplicity's sake.

<now-playing class="hidden">
  <div class="group max-w-sm" slot="container">
    <a
      href="#"
      slot="link"
      class="border border-gray-200 rounded-lg p-2 flex gap-4 items-center group-hover:bg-gray-200"
      target="_blank"
      rel="noopener noreferrer"
    >
      <span class="max-w-16">
        <img src="" slot="artwork" alt="" class="rounded-lg">
      </span>
      <span class="flex flex-col">
        <span slot="artist" class="text-xs text-gray-500"></span>
        <span slot="track" class="text-sm text-gray-900 font-bold"></span>
        <span slot="album" class="text-xs text-gray-500"></span>
      </span>
    </a>
  </div>
</now-playing>

Custom Element JavaScript

For the JavaScript, I'm using a custom element class to set up the element, fetch the data when it is displayed, and adjust the slots with real data. If I were making this for a more important production website, I'd make this a bit more robust and safe. (Add some code to check to make sure each of the slots exist before I update it, only fetch the data once per page, etc.) But that's not necessary for this site.

class NowPlaying extends HTMLElement {
  async connectedCallback() {
    await this.fetchNowPlaying()
  }

  async fetchNowPlaying () {
    const response = await fetch('https://nowplaying.johntornow.com/')

    // not playing
    if (response.status !== 200) {
      return
    }

    const json = await response.json()

    // not playing
    if (json.isPlaying !== true) {
      return
    }

    this.classList.remove('hidden')

    this.querySelector('[slot="album"]').innerText = json.album
    this.querySelector('[slot="artist"]').innerText = json.artist
    this.querySelector('[slot="track"]').innerText = json.track
    this.querySelector('[slot="artwork"]').src = json.artwork
    this.querySelector('[slot="artwork"]').alt = `${json.artist} - ${json.track}`
    this.querySelector('[slot="link"]').href = json.link
  }
}

customElements.define('now-playing', NowPlaying)

Cloudflare Worker

There's a fetch call in the JavaScript above to a subdomain that I'm using for a Cloudflare worker that handles the server logic. Cloudflare workers are simply amazing, and I love using them for little tools like this. This particular worker is just a file of JavaScript that runs whenever the root of the domain is requested.

In order to get the Spotify bits to work here, make sure to set up a new app on the Spotify Developer portal. Then, ensure you've copied the client id, and client secret into environment variables for the worker.

I also manually created a refresh token for myself using a standard Oauth 2 flow. I did this outside of the app just with Paw, but there's a bunch of other ways to get that value if need be.

A note on scopes: when setting up your initial access token, make sure to use the user-read-playback-state scope so you can read playback information.

There's one other path I took with the worker: some slight caching on the data. I don't know what limits Spotify has on this information, but I didn't like the idea of hitting its servers each page load. So for 3 minutes I'm storing the latest response from Spotify in Cloudflare's Workers KV storage. KV is very handy for this Varnish-style of caching. (I'm also storing my latest access token in KV as well, so each request doesn't need to re-authenticate.)

Since we're loading this worker from a client-side fetch request, I've wrapped all responses with some very loose CORS headers to allow it to work properly in the browser.

import { Buffer } from 'node:buffer'
import get from 'lodash.get'

// Get current access token from Spotify, using refresh token if need be
const fetchAccessToken = async (env) => {
  const clientId = env.CLIENT_ID
  const clientSecret = env.CLIENT_SECRET
  const originalToken = env.REFRESH_TOKEN

  const savedAccessToken = await env.spotify.get("access_token")

  if (savedAccessToken) {
    return savedAccessToken
  }

  const response = await fetch('https://accounts.spotify.com/api/token', {
    method: 'POST',
    headers: {
      'content-type': 'application/x-www-form-urlencoded',
      'Authorization': 'Basic ' + (new Buffer.from(clientId + ':' + clientSecret).toString('base64'))
    },

    body: new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: originalToken,
      client_id: clientId
    })
  })

  if (response.status !== 200) {
    return null
  }

  const json = await response.json()
  const newAccessToken = json.access_token

  // store the new token in KV store for an hour
  await env.spotify.put("access_token", newAccessToken, { expirationTtl: json.expires_in })

  return newAccessToken
}

const fetchSpotifyData = async (accessToken) => {
  console.debug('fetching current track from Spotify...')

  const response = await fetch('https://api.spotify.com/v1/me/player', {
    method: 'GET',
    headers: {
      'Authorization': `Bearer ${accessToken}`
    }
  })

  // only 200 OK status will continue
  if (response.status !== 200) {
    return null
  }

  const json = await response.json()

  // for debugging spotify response
  // console.log('spotify data:', json)

  const result = {
    isPlaying: false,
  }

  try {
    result.artist = get(json, 'item.artists[0].name')
    result.album = get(json, 'item.album.name')
    result.track = get(json, 'item.name')
    result.artwork = get(json, 'item.album.images[0].url')
    result.link = get(json, 'item.external_urls.spotify')
    result.isPlaying = json.is_playing
  } catch (e) {
    console.error('error parsing spotify data:', e)
  }

  return result
}

// to allow this to be loaded from my site (and any other really)
const responseWithCors = response => {
  response.headers.set('Access-Control-Allow-Origin', '*')
  response.headers.set('Access-Control-Allow-Methods', 'GET,OPTIONS,HEAD')
  response.headers.set('Access-Control-Max-Age', '86400')

  return response
}

export default {
  async fetch(request, env, ctx) {
    // check for a cached now playing track in KV
    const cachedCurrentTrack = await env.spotify.get("current_track")

    if (cachedCurrentTrack) {
      console.debug('Current track found in cache')

      const cachedData = JSON.parse(cachedCurrentTrack)
      return responseWithCors(Response.json(cachedData))
    }

    const accessToken = await fetchAccessToken(env)

    // if no token, we'll just skip the rest and assume nothing is playing
    if (!accessToken) {
      return responseWithCors(new Response(null, { status: 204 }))
    }

    const spotifyData = await fetchSpotifyData(accessToken)

    // spotify returns 204 no content if nothing playing, so we'll do the same
    if (spotifyData === null) {
      return responseWithCors(new Response(null, { status: 204 }))
    }

      // store the current track in KV for a few minutes for caching
    await env.spotify.put("current_track", JSON.stringify(spotifyData), { expirationTtl: (60 * 3) })

    return responseWithCors(Response.json(spotifyData))
  }
}

That's about it. There's some nuance to figure out with the Cloudflare workers for sure. But the basics are right here.

If you found this useful, let me know!

This concept was inspired by Cory Dransfeldt’s post about the same thing. Thanks Cory for the cool idea.

February 1, 2024

The Orioles are being sold

John Ourand, writing for my friends at Puck:

It looks like the Orioles sale is finally going to happen. I’ve had several plugged-in sources tell me that the team’s owner, John Angelos, has agreed to sell the franchise to a group led by two private equity billionaires: David Rubenstein, who started the Carlyle Group, hails from Baltimore, and has been tied to the deal for months; and Ares Management Corp. co-founder Mike Arougheti, who lives in New York. The extent of Arougheti’s involvement is unclear, but Rubenstein will become the “control person,” the term MLB uses for teams’ decision-makers. The deal values the club at $1.725 billion.

I never thought I’d see this day come. Is it a coincidence that it happened after the club’s most successful year since the early ‘80s? Probably not. Call me optimistic. This sounds great. (Can’t get any worse, to be honest.)

February 1, 2024

Adjusting Micro.blog

I wasn’t happy with how the cross-posting was going on Micro.blog. Especially for just external link posts, they looked a bit funny. I’ve added a secondary feed now that just posts these small status updates and any original posts. Links will continue to be on the main blog here. Steadily improving things as I go around here…

January 30, 2024

Arc Search

Arc Search is a new delightful and incredibly useful app from the Arc team. I keep trying to get used to Arc on my Mac but it hasn’t stuck for me. I just love the simplicity of Safari and it’s hard to shake for everyday browsing.

The mobile app is something entirely different. It’s a full browser to replace your default, yes, but it’s really focused on searching first and foremost. I love being able to type a search phrase and have the app “browse for me.” The “browse for me” feature summarizes the first few search results and makes a lovely compiled webpage for you to answer a question and give you links to sources.

Comparing this with a modern Google search result page is a breath of fresh air. No scrolling past 10 ads to get to an original content source. This is the best use of AI I’ve found for summarizing web search yet. It’s so cool and nicely designed. I don’t know if it’ll replace Safari on the phone for general browsing and reading, but I will definitely be using this for searching and research.

January 27, 2024

Week Notes: January 27, 2024

Happy Saturday. A few notes from the week about work and life…

We’re working on an integration with SMS notifications through Twilio. Most of its platform is really quite nice and a pleasure to work with. Switching between SMS and Whatsapp was incredibly easy.

Getting started with a new company on Twilio: not so easy though. I understand the need to prevent spam text messages and such, but it shouldn’t be so difficult to set up an account and get verified. I’ve had a relationship with this company for over a decade, and I use one of its acquired companies (SendGrid) to send over a million emails a week. A few dozen text messages shouldn’t be a problem!

If it’s so hard to get set up sending SMS messages, why do I still receive so many unsolicited political messages!?

Stripe continues to be the gold standard for third-party APIs. I wish every company would treat its developer community like Stripe. Incredible documentation. Thorough examples. Clear messages when things go wrong. Versioning and change notes that were written by humans. Lovely all around.

The third-party integration we dread updating? Shopify. The opposite of Stripe in every way. We’re forced every few months to update our API version through increasingly hostile means. The documentation is inconsistent, incomplete, and often just plain incorrect. No specific notes on what features are deprecated within our requests, just generic notes that what we’re doing is incorrect. It took a few folks on our team almost a week to upgrade our very basic usage of Shopify’s APIs. Not great! And, we’ll be forced to do it all over again in a few months.

Another great integration and provider of ours: Cloudflare. Incredible how much value we get from so little expense. One of our worker processes on Cloudflare served 24,697,912 requests this month. The cost? $3.60. Incredible.

Software timelines are nearly impossible to predict when creating something from nothing. This is a struggle on one of my small teams. We’re all working towards a goal, but it’s always a challenge to predict when things will be done. I’ve not found a tool or methodology that can help with this, and I’ve tried them all. Sometimes things are just going to take how long they’re going to take. And that’s okay.

Coffee with a friend this week was incredibly helpful for motivation and support. Starting, building, and running multiple software companies is a tough business. Some fresh perspective from a person that “gets it” meant the world.

And a few links for the week:

Spyglass – M.G. Siegler’s new site. Nice to see M.G. writing and putting out new content, I’ve always liked his work and opinions.

LM Studio – I’ve been messing with AI models quite a bit lately. It’s such an incredible new set of tools and technologies that’s changing our world. LM Studio is a super handy application that simplifies downloading and running various models locally on my Mac.

I know I’m biased here, but today’s issue of Air Mail is really great. The quality of work this team (not me, the editorial team!) is producing on a weekly basis is incredible and I’m so proud of this group. In this issue: my buddy Nathan King got an early look at the Vision Pro (jealous!) and Buzz Bissinger, author of Friday Night Lights, mourns the loss of Sports Illustrated.

Tomorrow afternoon: AFC Championship between the Chiefs and my beloved Ravens. Cautiously optimistic today. Really excited for this one. If you want to be the champs, you have to go through KC. Let’s see if Lamar and the boys can get it done.

And finally, March can’t come soon enough for me with the return of F1. This week we saw contract extensions for Charles Leclerc and Lando Norris. Glad to see the stability. Oh, and we have a new name for AlphaTauri: The Visa Cash App RB team. Just rolls off the tongue.

✌️❤️

January 25, 2024

The Mac at 40

Yesterday was the 40th anniversary of the introduction of the Macintosh computer. I can’t think of any device that has impacted my life more, even including the iPhone. In some hypothetical dystopian scenario where I could only keep one device, it would be my Mac. I switched to the Mac as soon as I could afford to do so and have been using it for over half of its 40 year history.

Happy Birthday, Mac. Many happy returns.

January 22, 2024

Sports Illustrated

Sad news last week reported by A.J. Perez at Front Office Sports:

Staffers at Sports Illustrated were notified on Friday of massive layoffs—some immediately, others in short time, with potential for the entire staff to be gone in three months.
Authentic, the licensing group that purchased Sports Illustrated for $110 million from Meredith five years ago, has terminated the agreement it holds with The Arena Group to publish SI in print and digital, according to an email obtained by Front Office Sports. That move comes three weeks after Arena missed a $3.75 million payment that breached the company’s SI licensing deal, which began in 2019. (Authentic’s notice of termination, meanwhile, triggered a $45 million fee due immediately to Authentic, according to an SEC filing on Friday.)
The fallout: On Friday Arena told SI employees in an email “… We were notified by Authentic Brands Group (ABG) that the license under which the Arena Group operates the Sports Illustrated (SI) brand and SI related properties has been officially revoked by ABG. As a result of this license revocation, we will be laying off staff that work on the SI brand.”

I enjoyed Peter King’s callout in his column this week about SI:

Nothing describes how the sports media business has changed better than the precipitous decline of Sports Illustrated. More than a bit of melancholy washed over me Friday, processing the news of the battered place. Because even if SI survives 2024, it will do so as a skeleton of what it was.
I have only good memories of my 29 years with the franchise. In the midst of the sadness and bitterness over SI’s demise, I want to share a few of the reasons why I will always consider myself the luckiest man on the face of the earth because I got to work for the greatest sports journalism franchise for the guts of my career.
I remember the phone call—absolutely, totally out of the blue—from managing editor Mark Mulvoy in spring 1989. I was 31, covering the Giants for Newsday. Mulvoy asked if I was interested in interviewing for a job at the magazine. It’s still one of those things to this day that I can’t quite believe happened. I went into the mag’s Rockefeller Center offices, across from Radio City, and Mulvoy got to the point pretty fast. He wanted me to write the “Inside the NFL” column and, in fact, there wasn’t much of an interview. He asked me if I wanted the job.

He goes on to ask a bunch of people about their favorite covers and SI memories. Really cool.

January 22, 2024

Notion Calendar

A new calendar app from Notion was announced this month. Notion’s calendar looks very nice. It appears to be (like Notion itself) a web-based calendar app but with a few native app wrappers. The integrations are nice: I was able to easily sign-in with my various Google Calendar accounts and also to a few Notion workspaces.

Overall it is a very nice little calendar app. Also nice that it’s just built into our existing paid Notion accounts so there’s nothing else to buy.

Design-wise it’s very clean and clear. One of the reasons I’m attracted to Notion is its clean design. The calendar fits right in.

The ability to connect to a Notion database in a calendar is really super nice and it’s something I really have wanted from Notion itself. I wish I didn’t need to use their calendar app to get my data out into a calendar. I’d much prefer to use Fantastical, but it doesn’t seem to be possible without third-party apps just yet.

It’s hard not to compare this launch with that of the HEY Calendar launched a few weeks earlier. Notion is a much more traditional approach to a calendar app. Both were easy to integrate with for external calendars. HEY’s approach is very different and thoughtful. Trying them both out for a few weeks to see which ideas stick.

January 22, 2024

NFL Divisional Round

Great weekend of football this weekend. The divisional round does not disappoint. The Ravens game was a bit stressful to begin the game, but they really turned it on during the second half. Good to see Lamar get a solid playoff win to silence his critics (for a few days, at least).

Sad to see the Bills lose last night. They played so well, but if you want to be the champs you’re going to have to beat the Chiefs sooner or later. Excited to see Jason Kelce at one more game this year.

Next weekend should be a lot of fun. I’m feeling optimistic about the Ravens taking care of business. Opening line is BAL -3. Basically a tie with the slight advantage going to home field advantage. I like it.