Open In App

Chrome Extension – Youtube Bookmarker

In this article, we’ll develop a chrome extension using which the user can create bookmarks corresponding to different timestamps, store them somewhere(for now in chrome local storage), and retrieve the bookmarks(stored as timestamped youtube video links).

The code is hosted here: GitHub. The video explainer for the main extension can be found here(in the main extension’s repo). Note that the project is still a Work in Progress at the time of writing this article.



Let’s get started. Broadly speaking, we’ll divide this article into 3 parts. In the first part, we’ll hook up the extension so that we can access our in-development extension in Chrome. In the second part, we’ll have a look at a basic extension and talk about the code and the architecture. In the third part, we’ll develop the main extension.

1. Testing our extension during development

To test and debug our extension during the development phase, we’ll follow the below steps to get a basic extension up and running.



  1. Create the manifest.json file and the project structure as shown above or download them from the link at the top of the article.
  2. Go to chrome://extensions from your browser. <ss here>
  3. Toggle the ‘Developer Mode’ to ON.
  4. Click on the ‘Load Unpacked’ option to load the Extension folder for test, debug and further development.
  5. At this point, you should see your Extension on the right side of the address bar of the chrome.

Browser screen snippet to show various options used to set up a functioning extension

Now that we have loaded our Extension folder on chrome, we are ready to build a basic chrome extension and test it out.

2. Basic Chrome Extension – Hi, there!

We’ll build an extension that will say, “Hi, there!” when the user clicks on the Extension icon. The code can be found here.

Clicking the ‘H’ icon opens the popup.html with a Hi, there! message

For that, we’ll need the following files.

1. manifest.json file

{
"name": "Hi, there! Extension",
"version": "1.0",
"description": "I greet",
"permissions": ["activeTab"],
"options_page": "options.html",
"background": {
"scripts": ["background.js"],
"persistent": false
},
"browser_action": {
"default_popup": "popup.html",
"default_title": "Hi, there Extension"
},
"manifest_version": 2
}

Let’s look at the key-value pairs in the manifest.json file in more detail.

  1. name – This is the name of the extension. In the browser screen snapshot, you can see this as ‘Hi, there! Extension.
  2. version – This is the version of the extension. It’s taken as 1.0 because when we upload it for review to the chrome web store, it would be 1.0 initially. In the development phase, you may name it as 0.0, then 0.1, and so on as well.
  3. description – Description of the extension. It’s a good practice to keep it concise.
  4. permissions – The permissions key-value pair holds the different permission that the Extension requires access to work properly. The value is an array of strings. The elements of the array can be either known strings, or a pattern match(usually used to match web addresses). For example, https://youtube.com/* -> gives permission to the extension for all links with domain name youtube.com. Here, the ‘*’ is a wildcard pattern. Also, note that some of the permissions declared here may need the user’s explicit approval as well. You can read more about permissions in the official docs.
  5. options_page – The options page is used to give a user more options. The user can access the options page by either right-clicking on the extension and click on the ‘options’ button from the menu, or going to the options page from the ‘chrome://extensions’ page. The setting used in the above manifest will cause the options.html page to open in a new tab. It is also possible to open the ‘options’ page in the same tab in an embedded manner. In the basic ‘Hi, there!’ extension, we don’t need this option. You can read more about the ‘options’ in the official docs. perhaps add more here
  6. background – background script(s) is declared here. The background script is used to listen to events and react to them, as the extension is an event-driven paradigm. A background page is loaded when it is needed and unloaded when it goes idle, i.e., the background script will keep running while it is performing an action and unload after the action is performed. In our ‘Hi, there!’ extension, we haven’t used the background script functionality. You can also notice that the “persistent” key is set to false because it’s the standard practice, and as mentioned in the docs, the only time we should set “persistent” to true is when the extension is using the chrome.webRequest API to block or modify network requests, which we’re not doing.
  7. browser_action – The browser_action is used by the Extension to put the icon on the right side of the chrome’s address bar. The extension then listens for clicks, and when the icon is clicked, it does something. In our case, it opens the popup.html page containing the Hi, there! greeting. The ‘default_title’ field value is a string that is displayed when the user hovers over the extension’s icon.
  8. manifest_version – At the time of writing this article, developers are being encouraged to try out the soon-to-be-launched manifest_version 3. However, version 2 is still available and familiar, so we’ll use version 2. For version 3, you can start here.

 

2. popup.html file




<!DOCTYPE html>
<html>
  <head></head>
  <body>
    <h5>Hi, there!</h5>
    <script src="jquery-3.5.1.js">
    </script><script src="popup.js">
    </script>
  </body>
</html>

How does the Basic Chrome Extension work?

After we’ve ‘load unpacked’ the chrome extension, the manifest.json file is processed. After this, the ‘background’ is run to initialize the extension and for listening to other events. Now, that we’ve got an understanding of chrome extension from the development point of view, we’ll look at the main Extension that we want to build – YouTube Bookmarker Extension.

Main Extension – YouTube Bookmarker

The video demo for the YouTube Bookmarker extension can be found here.
 

Features of the Extension:

 

Project Directory structure:
 

project directory structure

 

Source Code:

1. As the extension would access and store the web addresses, i.e., youtube video addresses, it becomes imperative to have a look at the different kinds of timestamps that we can access from the DOM.

If one runs

var result1 = document.querySelector('#movie_player >
div.ytp-chrome-bottom > div.ytp-progress-bar-container >
div.ytp-progress-bar').getAttribute("aria-valuetext");

It will give the timestamp in word format, as defined below. You are being encouraged to run the above query selector in the chrome’s window console accessible by pressing ‘Ctrl+Shift+J’.

The below are the types of timestamps that can be retrieved in word format by running the query selector above.

1 Hours 48 Minutes 31 Seconds of 2 Hours 56 Minutes 33 Seconds
//for video greater than 1 hours long and the current
//timestamp being greater than 1 hour too.


-> 0 Minutes 46 Seconds of 2 Hours 56 Minutes 33 Seconds
//for video greater than 1 hours long and
//current timestamp < 1 minute

-> 8 Minutes 33 Seconds of 2 Hours 56 Minutes 33 Seconds
//for video greater than 1 hours long and
//current timestamp >= 1 minute and less than 1 hour

-> 0 Minutes 0 Seconds of 0 Minutes 56 Seconds
//for video less than 1 minute long and
//current timestamp < 1 minute

2. manifest.json:

{
"name": "Youtube video bookmarker",
"version": "1.0",
"description": "An extension that bookmarks time points in youtube video",
"permissions": ["activeTab", "declarativeContent",
         "storage", "tabs", "https://www.youtube.com/*"],
"options_page": "options.html",
"background": {
"scripts": ["background.js"],
"persistent": false
},
"externally_connectable": {
"matches": ["*://*.youtube.com/*"]
},
"browser_action": {
"default_popup": "popup.html",
"default_title": "YouTube Video Bookmarker - GFG"
},
"manifest_version": 2
}

The fields of manifest.json file are defined below:

3. popup.html:




<!DOCTYPE html>
<html>
  <head></head>
  <body>
    <div>
      <div id="bookmarktakerdiv">
        <h5 style="color:darkgreen">Youtube video Bookmarker</h5>
        <span id="currts">xx:xx</span>
        <span id="receiptts" style="color:darkgreen"></span>
        <input type="text" id="bookmarkdesc" />
        <button id="submitbookmark" type="button">Bookmark</button>
      </div>
      <hr /><div id="bookmarklistdiv">
        
<p>Bookmarked points</p>
  
      <ul id="bookmark_ulist">
          
 <!-- <li><span id="ts">xx:xx</span>
<span id="note">example desc</span></li>
<li><span id="ts">xx:xx</span>
<span id="note">example desc</span>
</li><li><span id="ts">xx:xx</span>
<span id="note">example desc</span></li> -->
      </ul></div>
    </div>
    <script src="jquery-3.5.1.js"></script>
    <script src="popup.js"></script>
      
    <!-- <script src="options.js" /> -->
  </body></html>

Here we have the implementation of the popup.html file:

 

4. popup.js:




'use strict';
  
$(function() {
  
        //retrieve data from local for already stored notes
        // chrome.runtime.sendMessage({ method: "getbookmarks" })
          //todo, not connected to background.js
  
        // $('#bookmark_ulist > li > span > a').on("click", function() {
        // console.log('li clicked event fired');
    }) //todo
  
//todo
//make same page reload of youtube video to bookmarked point
  
$('#bookmarkdesc').focus(function() {
  
        console.log('focus bookmark description input field') //executing
  
        //for sending a message
        chrome.runtime.sendMessage({ method: "gettimestampforcurrentpoint" });
  
        //for listening any message which comes from runtime
        chrome.runtime.onMessage.addListener(tsvalue);
  
  
  
        var ts, tslink;
  
        function tsvalue(msg) {
            // Do your work here
            if (msg.method == "tsfind") {
                ts = msg.tsvaltopopup;
                tslink = msg.fl;
  
                // console.log('ts tslink' + ts + ' ' + tslink) 
                $('#submitbookmark').on('click', function() {
                    // console.log('submitnote button clicked')
                    //#bookmark_ulist
  
                    var bookmarkinput = $('#bookmarkdesc').val();
  
                    // console.log('#bookmarkinput val ' + bookmarkinput); 
                    $('#bookmark_ulist').append('<li><span>' + ts +
                                                ' - <a href="' + tslink + '">' +
                                                bookmarkinput + 
                                                '</a></span></li>');
                    console.log('list item appended to bookmark_ulist')
  
                // chrome.storage.local.set({ "bklocal": bookmarkinput,
                // "tslocal": ts, "vidlinklocal": tslink })
  
                //popup > bg > fg while setting
                //while getting, see..
                // chrome.runtime.sendMessage({ method: "setlocalstorage",
                // bookmarkvalue: bookmarkinput, timestamp: ts, vidlink: tslink})
  
  
                });
  
                $('#currts').text(msg.tsvaltopopup)
                $('#receiptts').text('got timestamp')
  
  
                // makeentryinstorage(bookmarkinput, ts, tslink);
  
            }
        }
  
  
    })
    // });
  
  
  
// chrome.storage.local.set({ note: inputnote,
//timestamp: time, videolink: link });
  
// function makeentryinstorage(bookmarkinput, time, link) {
//chrome.runtime.sendMessage({ method: "storeinlocal", note: bookmarkinput,
// timestamp: time, videolink: link });
  
// } //todo
  
// makeentryinstorage(bookmarkinput, ts, tslink);
  
  
// $('#pointsli').append('<li><span><a href="' + $(msg.fl) + '">' +
// noteinput + '</a></span></li>');
  
  
// console.log('msg obj popup.js ' + msg);
// console.log('popupjs noteinput ' + noteinput);
// $('#pointsli').append('<li><span><a href="' + $(msg.fl) + '">' + 
// noteinput + '</a></span></li>');
  
// { method: "tsfind", tsvaltopopup: msg.tsval, fl: msg.finallink }

The below list explains the approach we took for popup.js:

 

5. background.js




"use strict";
  
// to check connection of fg with bg
  
chrome.runtime.onMessage.addListener(checktimestamp);
  
function checktimestamp(msg) {
  // Do your work here
  if (msg.method == "gettimestampforcurrentpoint") {
    console.log("bg.js gettimestampforcurrrentpoint called");
    chrome.tabs.executeScript(null, { file: "./gettimestamp.js" }, () => {
      console.log("injected gettimestamp.js file into YT window DOM.");
      // gettimestamp.js will execute now in main chrome window which
      // is running youtube.com/somevideo
    });
  }
}
  
// first this runs
chrome.tabs.onActivated.addListener((tab) => {
  console.log(tab);
  chrome.tabs.get(tab.tabId, (c) => {
    // console.log(c.url);
    if (/^https:\/\/www\.youtube/.test(c.url)) {
      // above pattern tests for the youtube hostname.
      // If youtube is running in the active tab,
      // it injects ./foreground.js in DOM.
  
      chrome.tabs.executeScript(null, { file: "./foreground.js" }, () => {
        console.log("i injected fg using bg script in youtube webpages");
      });
    }
  });
  
  // fetch data from local storage
  // var windowlink;
  // chrome.tabs.get(tab.tabId, a => {
  //     windowlink = a.url;
  // });
  // console.log('testretrieval bg.js')
  // chrome.runtime.sendMessage({ method: "testretrieval" });
});
  
// chrome.browserAction.onClicked.addListener(function(tab) {
//     console.log('browser action called' + tab);
//     //     // Run the following code when the popup is opened
// });
  
//for sending a message
// chrome.runtime.sendMessage({greeting: "hello"}, function(response) {
  
// });
  
// chrome.runtime.onMessage.addListener(retrievenotes)
  
// function retrievenotes(msg) {
//     if (msg.method == "getnotes") {
//         //todo
//     }
  
// }
  
// chrome.runtime.onMessage.addListener(storelocal); //todo
  
// function storelocal(msg) { //todo
//     if (msg.method == "storeinlocal") {
//         chrome.storage.local.set({ note: inputnote, timestamp: time,
//          videolink: link });
//     }
// }
  
chrome.runtime.onMessage.addListener(getcurrenttimestamp);
  
function getcurrenttimestamp(msg) {
  if (msg.method == "sendtimestamptobg") {
    var temp1 = msg.tsvalue;
    var temp2 = msg.finallink;
    console.log("msg.tsvalue value: " + msg.tsval);
    console.log("msg.finallink " + msg.finallink);
    //tsval and finallink being received properly in the bg consolelog
  
    chrome.runtime.sendMessage({
      method: "tsfind",
      tsvaltopopup: temp1,
      fl: temp2,
    });
    // , function() {
    //     console.log('tsval to popup');
    // })
  }
}
  
chrome.runtime.onMessage.addListener(localstorageset);
  
function localstorageset(msg) {
  if (msg.method == "setlocalstorage") {
    console.log("setlocalstorage background.js"); //called
    // chrome.runtime.sendMessage({ method: "setlocalstorage",
    // bookmarkvalue: bookmarkinput, timestamp: ts, vidlink: tslink })
    // chrome.storage.local.set({ "bklocal": msg.bookmarkvalue,
    // "tslocal": msg.timestamp, "vidlinklocal": msg.vidlink })
    // chrome.storage.local.set({ "password": "123" })
    // chrome.runtime.sendMessage({ method: "localstoragesetrequest",
    // pass: "hellopass" });
  }
}
  
// chrome.runtime.onInstalled.addListener(function() {
//   chrome.storage.sync.set({color: '#3aa757'}, function() {
//     console.log('The color is green.');
//   });
//   chrome.declarativeContent.onPageChanged.removeRules(undefined, function() {
//     chrome.declarativeContent.onPageChanged.addRules([{
//       conditions: [new chrome.declarativeContent.PageStateMatcher({
//         pageUrl: {hostEquals: 'developer.chrome.com'},
//       })],
//       actions: [new chrome.declarativeContent.ShowPageAction()]
//     }]);
//   });
// });

Let’s check out what we did in the background.js file:

 

6. gettimestamp.js




var result1 = document
  .querySelector(
    "#movie_player > div.ytp-chrome-bottom >div.ytp-progress-bar-container >div.ytp-progress-bar"
  )
  .getAttribute("aria-valuetext");
//example of result1 is 1 Hours 48 Minutes 31 Seconds of 2 Hours 56 Minutes 33 Seconds.
// Here, the timestamp is 01:48:31 in hh:mm:ss format out of a 02:56:33 long video.
  
// construct link to exact point here
var temparr = result1.split(" ");
  
var tshhmmss_string;
  
if (temparr[6] == "Hours") {
  tshhmmss_string = +"00:" + temparr[0] + ":" + temparr[2];
} else if (temparr[1] == "Hours") {
  tshhmmss_string = temparr[0] + ":" + temparr[2] + ":" + temparr[4];
} else if (temparr[6] == "Minutes") {
  tshhmmss_string = "00:" + temparr[0] + ":" + temparr[2];
}
  
console.log("gettimestamp.js " + result1);
  
var windowlink = window.location.href;
// can be stored in pagelink.
  
console.log("gettimestamp.js windowlink " + windowlink);
  
// find index of v= substring
var idx = windowlink.indexOf("v=");
// return index from where the 'v=' substring starts in the windowlink.
// For example, in https://www.youtube.com/watch?v=JaIU4CteN50, idx = 30.
// indexOf function returns -1 if the substring is not found in the string.
  
console.log("gettimestamp.js idx value " + idx);
  
// console.log('tres gettimestamp.js ' + tres); fine - format hh:mm:ss
  
// console.log('typeof tres gettimestamp.js ' + typeof(tres));
//working fine - returns string
  
function getseconds(timestamphhmmss) {
  var x = timestamphhmmss.split(":");
  var seconds = parseInt(x[0]) * 60 * 60 + parseInt(x[1]) * 60 + parseInt(x[2]);
  console.log("seconds calculated gettimestamp.js getinseconds" + seconds);
  return seconds;
} //function working fine
  
var timeinseconds = getseconds(tshhmmss_string); //working fine - returns number
  
// console.log('tsinsec gettimestamp.js ' + tsinsec); fine
  
var windowlinkfinal;
  
if (idx == -1) {
  windowlink = "https://youtube.com";
  //in case of substring not found, i.e. bad case, store youtube.com as default.
} else {
  windowlinkfinal =
    windowlink.substr(idx + 2, 11) +
    "&t=" +
    timeinseconds;
  console.log("gettimestamp.js windowlinkfinal " + windowlinkfinal);
} // pagelinkfinal fine
  
chrome.runtime.sendMessage({
  method: "sendtimestamptobg",
  tsvalue: tshhmmss_string,
  finallink: windowlinkfinal,
});

Let’s check out what we did in the gettimestamp.js file above:

The timestamp in hh:mm:ss format and the timestamped video link are emitted for consumption by background.js file. The event is identified by ‘method: “sendtimestamptobg”‘ key.

Further Development Scope for you to try out

Feel free to raise a PR or issue on the repo. The immediate WIP is connecting it to chrome.localStorage.

 


Article Tags :