Slow Down × Terrace House: Writing a Browser Extension to Change a Theme Song on Netflix

Slow Down Terrace House

Slow Down × Terrace House

I just finished binging the latest release of Terrace House, a reality TV show from Japan. If you’re not familiar, Terrace House is hands down the slowest and quietest reality television you’ll find, although not without its own kind of drama. And while the fourth installment, Opening New Doors, kept a lot of what made the show an international hit, there’s one big problem: they changed the opening theme song.

The new song is fine. The problem is the song used in earlier series, “Slow Down” by Lights Follow, was a perfect fit. It set the mood for each episode with its theme of figuring yourself out and its relaxed, bittersweet vibe. I’m not the only one to feel this way. One fan went so far as to re-cut the new intro with the old theme song.

Well, I decided to go a step further by writing a browser extension for Chrome, Slow Down × Terrace House, that switches the theme song back for new episodes of Terrace House on Netflix. You can try it out on the Chrome Web Store:

Here I wanted to share my general approach and some specifics for anyone interested in making a similar browser extension. Maybe this will put you on the path to giving every season of The Wire your favorite rendition of “Way Down in the Hole”. Or blasting “Immigrant Song” over every battle scene involving Thor. The possibilities are endless.

The Plan

The general plan for the extension is simple:

  1. Check if an episode of Terrace House: Opening New Doors is playing on Netflix.
  2. Detect when the opening credits are about to start.
  3. Mute the Netflix player and play “Slow Down” instead.
  4. Unmute the Netflix player when the credits end.

As for knowing when the opening credits start, it unfortunately varies quite a bit by episode, starting as late as 17 minutes in. With only 16 episodes so far (and finding no better solution), I took the brute-force approach of hard-coding timestamps for all of the episodes. This added a step 1b for detecting the episode number and looking up the timestamp.

Inside the Extension

With this plan in hand, I created the skeleton of the Chrome extension by diving into the docs. I recommend you do the same.

But in a nutshell, there are two contexts: content scripts are injected into the active web page, in this case Netflix; and background scripts, which you can think of as your extension’s own context, which can detect browser actions like adding a bookmark and switching tabs, as well as communicate with your content scripts.

The content and background scripts, along with the extension’s metadata, are defined in a manifest.json file. Here’s what it looks like:

{
  "name": "Slow Down x Terrace House",
  "short_name": "SD x TH",
  "version": "1.0.0",
  "manifest_version": 2,
  "description": "Replace the Terrace House theme song on Netflix with \"Slow Down\" by Lights Follow.",
  "homepage_url": "https://thomaspark.co",
  "icons": {
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  },
  "content_scripts": [
    {
      "matches": ["https://www.netflix.com/*"],
      "js": ["content.js"]
    }
  ],
  "background": {
    "scripts": ["background.js"],
    "persistent": false
  },
  "permissions": ["webNavigation"],
  "web_accessible_resources": ["audio/slowdown.mp3"]
}

Detecting Series, Season, Episode

Detecting when Terrace House is being watched and grabbing the episode number happens in, you guessed it, the content script. Here, the script returns early if it’s on something other than an active video page (for example, the main menu or episode list), and keeps checking the page until the video is loaded. Then it grabs the series, season, and episode from the DOM.

function init() {
  if (!window.location.href.startsWith("https://www.netflix.com/watch/")) {
    return;
  }

  const videoCheckInterval = setInterval(function() {
    const video = document.querySelector(".VideoContainer video");
    const titleElem = document.querySelector(".video-title h4");

    if (video !== null && titleElem !== null) {
      clearInterval(videoCheckInterval);

      const title = document.querySelector(".video-title h4").innerText;

      if (title.startsWith("Terrace House")) {
        const id = video.parentElement.id;
        const series = title.split(":")[1].trim();
        const [season, episode] = document.querySelector(".video-title h4").nextSibling.innerText.split(":");
        // The fun starts here.
      }
    }
  }, 10);
}

Messaging Between Contexts

Once a Terrace House: Opening New Doors episode is detected, the content script needs to tell the background script to set up the “Slow Dive” audio. This starts by opening a connection and then passing a message.

In content.js, you can add the following:

const port = chrome.runtime.connect({name: "slowdown"});

And wherever you need it in the meat of your function:

port.postMessage({action: "play"});

In background.js, you’re listening for the connection and the play message:

chrome.runtime.onConnect.addListener(function(port) {
  const audio = new Audio("audio/slowdown.mp3");

  port.onMessage.addListener(function(msg) {
    if (msg.action == "play") {
      audio.play();
    }
  }
}

Keep in mind that you can also send messages in the other direction, from background.js to content.js.

Controlling Video & Audio

If you’ve been paying close attention to the code snippets above, you’ll notice that we already have our references to the video element on Netflix’s site in content.js and the audio element for “Slow Down” in background.js. We can control these elements using JavaScript with some handy methods.

To mute and unmute:

video.muted = true;
video.muted = false;

To play and pause:

audio.play();
audio.pause();

To get and set the current time:

const time = video.currentTime;
audio.currentTime = 500;

The video and audio elements also fire events. For example, in inject.js, this code will listen for when the video is paused, then send a message to background.js to also pause the song.

video.addEventListener("pause", function() {
  port.postMessage({action: "pause"});
});

And then in background.js:

if (msg.action == "pause") {
  audio.pause();
}

Another valuable event is timeupdate, which fires every time the play position of a video or audio file has changed. Use this to continuously check whether the opening credits start. Here’s a simplified version:

video.addEventListener("timeupdate", function() {
  if (video.currentTime === START_TIME) {
    video.muted = true;
    port.postMessage({action: "play"});
  }
}

This is pretty much everything you need. Put it all together and you have the basic functionality of the extension.

With that said, there are a lot of edge cases and other details to account for that’ll considerably complicate your code. For example, in past series, the “Slow Down” theme song would begin fading in over the dialogue. To replicate this, you need to start playing the song first, then muting the video after a ten second delay, instead of simply having both events coincide.

Another hurdle I encountered is that Netflix behaves as a single-page app. When you navigate from one episode to another on Netflix’s site, a new page isn’t loaded, meaning whatever initialization your extension needs to make on the new video won’t run again. To work around this, background.js needs to listen for when history.pushState() is used by Netflix to display a new URL, and then send a message to content.js to re-initialize.

chrome.webNavigation.onHistoryStateUpdated.addListener(function() {
  audio.pause();
  port.postMessage({action: "init"});
});

Publishing to the Chrome Web Store

Once you’ve finished your extension, you’ll want to distribute the fruits of your labor through the Chrome Web Store. Pay the one-time developer fee of $5 and you can submit your extension. The process is short and sweet. Zip and upload your extension, then fill out some fields. Your listing will go live in a few minutes.

If you’re a fan of Terrace House, I hope you enjoy the Slow Down × Terrace House extension.

Cue closing door sound.

Terrace House: Opening New Doors

Leave a Reply