Skip to content

Instantly share code, notes, and snippets.

@ryanmuller
Created June 14, 2020 16:41
Show Gist options
  • Save ryanmuller/9cd8df0d5d3dd4b51cabf7e1e255c33f to your computer and use it in GitHub Desktop.
Save ryanmuller/9cd8df0d5d3dd4b51cabf7e1e255c33f to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Roam YouTube timestamp controls
// @namespace learnstream.org
// @version 0.1
// @description Add timestamp controls to YouTube videos embedded in Roam
// @author Ryan Muller <[email protected]>
// @match https://*.roamresearch.com
// @grant none
// ==/UserScript==
// Copyright 2020 Google LLC.
// SPDX-License-Identifier: Apache 2.0
(function() {
'use strict';
let ytApiReady = false;
const players = new Map();
const activateYtVideos = () => {
if (!ytApiReady) {
if (window.YT !== undefined) loadYtApi(); // wait until Roam loads its YT, then insert script on top
return null;
}
Array.from(document.getElementsByTagName('IFRAME'))
.filter(iframe => iframe.src.includes('youtube.com'))
.forEach(ytEl => {
const ytId = ytEl.src.split('/')[4].split('?')[0];
const block = ytEl.closest('.roam-block-container');
if (!block.classList.contains('youtube-activated')) {
const parent = ytEl.parentElement;
parent.id = 'player-' + players.size;
block.classList.add('youtube-activated');
block.dataset.ytId = ytId;
ytEl.remove();
players[ytId] = new window.YT.Player(parent.id, {
height: '300', width: '450', videoId: ytId});
}
addTimestampControls(block, players[ytId]);
});
};
const loadYtApi = () => {
const tag = document.createElement('script');
tag.src = 'https://www.youtube.com/iframe_api';
const firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
window.onYouTubeIframeAPIReady = () => { ytApiReady = true; };
};
const addTimestampControls = (block, player) => {
if (block.children.length < 2) return null;
const childBlocks = Array.from(block.children[1].children);
childBlocks.forEach(child => {
const timestamp = getTimestamp(child);
const buttonIfPresent = child.classList.contains('timestamp-activated') ? getControlButton(child) : null;
const timestampChanged = buttonIfPresent !== null && timestamp != buttonIfPresent.dataset.timestamp;
if (buttonIfPresent !== null && (timestamp === null || timestampChanged)) {
buttonIfPresent.remove();
child.classList.remove('timestamp-activated');
}
if (timestamp !== null && (buttonIfPresent === null || timestampChanged)) {
addControlButton(child, () => player.seekTo(timestamp, true));
getControlButton(child).dataset.timestamp = timestamp;
child.classList.add('timestamp-activated');
}
});
};
const getControlButton = (block) => block.querySelectorAll('.timestamp-control')[0];
const addControlButton = (block, fn) => {
const button = document.createElement('button');
button.innerText = '►';
button.classList.add('timestamp-control');
button.addEventListener('click', fn);
button.style.marginRight = '8px';
const parentEl = block.children[0].children[0];
parentEl.insertBefore(button, parentEl.querySelectorAll('.roam-block')[0]);
};
const getTimestamp = (block) => {
const innerBlockSelector = block.querySelectorAll('.roam-block');
const blockText = innerBlockSelector.length ? innerBlockSelector[0].textContent : '';
const matches = blockText.match(/^((?:\d+:)?\d+:\d\d)\D/); // start w/ m:ss or h:mm:ss
if (!matches || matches.length < 2) return null;
const timeParts = matches[1].split(':').map(part => parseInt(part));
if (timeParts.length == 3) return timeParts[0]*3600 + timeParts[1]*60 + timeParts[2];
else if (timeParts.length == 2) return timeParts[0]*60 + timeParts[1];
else return null;
};
setInterval(activateYtVideos, 1000);
})();
@abhayprasanna
Copy link

Hi Ryan - thanks for this amazing tool! I've applied it via [[roam/js]] and it works great. One thing I've noticed is that if I go to the next block and shift select up to the previous block that has a timestamp that has been processed already, another play icon gets created, and this can keep happening (I stopped after 6-7 times). Perhaps a bug?

@lyrium
Copy link

lyrium commented Jul 1, 2020

No solutions, though I reproduced it. Interestingly, if I backspace, Roam throws an error and on reload the entire block is gone.

Screenshot 2020-07-01 at 10 57 04 PM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment