How to create Google Chrome Extensions using the Netflix subtitles styler?

Today we will create Google Chrome extensions for manipulating Netflix subtitle styles in real time. You will find information about creating extensions from scratch, some practical advice and general views on extension architecture. If you are not happy with the available Netflix subtitles options or just want to quickly create some extension to make life easier, this article is for you.

Our goals:

– create extension logic

– store settings in the browser Local Storage

– autoload and activate extensions only on the Netflix page

– create popup menu

– create forms with subtitles options

Requirements:

– basic knowledge of HTML, CSS and JavaScript

Netflix by its API sends every subtitle sentence separately. It uses CSS styles for styling subtitles. With access to the page DOM we can manipulate those received styles with Chrome extension.

The manifest

Firstly, we have to create the manifest file called manifest.json. This tells the browser about the extension setup, such as the UI files, background scripts and the capabilities the extension might have.

Here is a complete manifest.

{
  "name": "Netflix subtitles styler",
  "version": "1.0",
  "description": "Netflix subtitles styler",
  "author": "twistezo",
  "permissions": ["tabs", "storage", "declarativeContent", "https://*.netflix.com/"],
  "background": {
    "scripts": ["background.js"],
    "persistent": false
  },
  "page_action": {
    "default_popup": "popup.html",
    "default_icon": "logo.png"
  },
  "manifest_version": 2
}

As you see, we have some standard information, such as name, version, description, homepage_url and manifest_version.

One of the important parts of the manifest is the permissions section. This is an array with elements that our extension can access.

In our case, we need to have access to tabs to find the active tab, execute scripts and send messages between the UI and the extension. We need storage for store extension settings in the browser and declarativeContent for taking action depending on the tab content. The last element https://*.netflix.com/ allows extension access only to the netflix.com domain.

Chrome extensions have a separate logic from the UI so we need to have background.scripts, which tells the extension where it can find its logic. persistent: false means that this script will be used only if needed. page_action is the section with the UI part. We have here a simple HTML file for a popup menu and an extension’s PNG logo.

Extension logic

First we have to setup runtime.onInstalled behaviours, remove any current rules (for example from older versions) and declare function to add new rules. We use Local Storage for the storage settings so we can allocate default settings after the extension is installed.

We will be using three subtitle style parameters:

    • vPos – vertical position from bottom [px]
    • fSize – font size [px]
    • fColor – font color [HEX]

Create background.js:

chrome.runtime.onInstalled.addListener(() => {
  chrome.storage.local.set({ vPos: 300, fSize: 24, fColor: "#FFFFFF" });

  chrome.declarativeContent.onPageChanged.removeRules(undefined, () => {
    chrome.declarativeContent.onPageChanged.addRules([
      // array with rules
    ]);
  });
});

Our rule goal is to disable the extension button on all domains other than netflix.com. We create new rule with PageStateMatcher conditions and declare ShowPageAction where new rule will be assigned.

{
  conditions: [
    new chrome.declarativeContent.PageStateMatcher({
      pageUrl: { hostSuffix: "netflix.com" }
    })
  ],
  actions: [new chrome.declarativeContent.ShowPageAction()]
}

The next step is to add tabs.onUpdated listener, which will execute our script while loading or refreshing the active tab.

{
  conditions: [
    new chrome.declarativeContent.PageStateMatcher({
      pageUrl: { hostSuffix: "netflix.com" }
    })
  ],
  actions: [new chrome.declarativeContent.ShowPageAction()]
}

Firstly we check that changeInfo.status has the status complete. This means that the website on this tab is loaded. Then we get settings from Local Storage and declare which script should be run on the current tab with tabId. Finally, in callback we send the message with settings from the UI to the script.

Extension UI

To create an extension popup menu with form, we create three files: popup.html and popup.css with visual layers and popup.js with logic for communicating between the menu and isolated background.js script.

Our UI goal:

Netflix styler

Here we have a simple HTML form with built-in validation: popup.html:

<!DOCTYPE html>
<html>
  <head>
    <link href="https://fonts.googleapis.com/css?family=Open+Sans:400,600" rel="stylesheet" />
    <link rel="stylesheet" href="popup.css" />
  </head>
  <body>
    <div class="container logo">
      NETFLIX SUBTITLES STYLER
    </div>
    <form id="popup-form" class="container">
      <div class="input-info">Vertical position from bottom [px]</div>
      <input class="form-control" id="vPos" type="number" value="" min="0" max="5000" />
      <div class="input-info">Font size [px]</div>
      <input id="fSize" type="number" value="" min="0" max="300" />
      <div class="input-info">Font color [HEX]</div>
      <input id="fColor" type="text" value="" pattern="^#[0-9A-F]{6}$" />
      <button id="change" type="submit">Change</button>
    </form>
    <div class="container footer">
      &copy; twistezo, 2019
    </div>
    <script src="popup.js"></script>
  </body>
</html>

Styling the popup menu is not the goal of this article, so I suggest you visit https://github.com/twistezo/netflix-subtitles-styler and copy the whole popup.css file into your project.

UI logic – popup.js:

const form = document.getElementById("popup-form");
const inputElements = ["vPos", "fSize", "fColor"];

chrome.storage.local.get(inputElements, data => {
  inputElements.forEach(el => {
    document.getElementById(el).value = data[el];
  });
});

form.addEventListener("submit", event => {
  event.preventDefault();
  const [vPos, fSize, fColor] = [...inputElements.map(el => event.target[el].value)];

  chrome.tabs.query({ active: true, currentWindow: true }, tabs => {
    chrome.storage.local.set({ vPos, fSize, fColor });
    chrome.tabs.executeScript(
      tabs[0].id,
      {
        file: "script.js"
      },
      () => {
        const error = chrome.runtime.lastError;
        if (error) "Error. Tab ID: " + tab.id + ": " + JSON.stringify(error);

        chrome.tabs.sendMessage(tabs[0].id, { vPos, fSize, fColor });
      }
    );
  });
});

In above script, we load settings from Local Storage and attach them to form inputs. Then we create listener to submit event with functions for the save settings to Local Storage and send them by message to our script. As you see, we use Local Storage in every component. The Chrome extension doesn’t have its own data space so the simplest solution is to use browser local space like Local Storage. We also often use the sendMessage function. It’s caused by Chromme extensions’ architecture – they have separate logic from the UI.

The script

Now it is time to create script.js with logic for manipulating Netflix subtitles styles.

First, we create onMessage listener for receiving messages with settings from the extension.

chrome.runtime.onMessage.addListener((message, _sender, _sendResponse) => {
  // function for manipulating styles
});

Then in the same file we create the function for changing proper Netflix styles to our styles in real time.

changeSubtitlesStyle = (vPos, fSize, fColor) => {
  console.log("%cnetflix-subtitles-styler : observer is working... ", "color: red;");

  callback = () => {
    // .player-timedText
    const subtitles = document.querySelector(".player-timedtext");
    if (subtitles) {
      subtitles.style.bottom = vPos + "px";

      // .player-timedtext > .player-timedtext-container [0]
      const firstChildContainer = subtitles.firstChild;
      if (firstChildContainer) {
        // .player-timedtext > .player-timedtext-container [0] > div
        const firstChild = firstChildContainer.firstChild;
        if (firstChild) {
          firstChild.style.backgroundColor = "transparent";
        }

        // .player-timedtext > .player-timedtext-container [1]
        const secondChildContainer = firstChildContainer.nextSibling;
        if (secondChildContainer) {
          for (const span of secondChildContainer.childNodes) {
            // .player-timedtext > .player-timedtext-container [1] > span
            span.style.fontSize = fSize + "px";
            span.style.fontWeight = "normal";
            span.style.color = fColor;
          }
          secondChildContainer.style.left = "0";
          secondChildContainer.style.right = "0";
        }
      }
    }
  };

  const observer = new MutationObserver(callback);
  observer.observe(document.body, {
    subtree: true,
    attributes: false,
    childList: true
  });
};

For Netflix, every time it receives whole subtitle sentences it swaps only the subtitles part of the page DOM. So we have to use an observer function like MutationObserver, which will be triggering our changeSubtitlesStyle function every time when the page DOM has changed. In the callback function we see simple manipulation of styles. The commented lines have information about where you can find proper styles.

Time to run

I assume that you do not have a developer account in Chrome Webstore. So to run this extension go to chrome://extensions/ in your Chrome, click the Load unpacked, select folder with the extension and that’s it! Then, obviously go to the Netflix page for testing it.

Conclusions

As you see, it is easy to start creating some extensions that make life easier. The most important part is to understand Google Chrome Extension divided architecture and communication between components. This subtitles styler is only a simple demo of what you can do with the Chrome Extension API.

Useful links:

– Repository with this project https://github.com/twistezo/netflix-subtitles-styler

– Official Google guide https://developer.chrome.com/extensions/overview

– Chrome Extension API https://developer.chrome.com/extensions/api_index

Read more:

– Codest’s good practice for building software: CircleCI

– Germany is a great place for startups: a well-designed startup ecosystem

– Why Scrum works and has visible effects on software projects?

Did you like it? Share this article

Next

Let's start a project

Estimate project