Skip to content

Instantly share code, notes, and snippets.

@jlongtine
Last active November 28, 2020 08:18
Show Gist options
  • Save jlongtine/75305d2080bb4ba1f9206269e4ba7acc to your computer and use it in GitHub Desktop.
Save jlongtine/75305d2080bb4ba1f9206269e4ba7acc to your computer and use it in GitHub Desktop.
youtube-timestamp.ts

You can add this to Roam using {{[[roam/js]]}}

Grab the code in youtube-timestamps.js and drop it in a javascript code block:

  ```javascript```

You can have timestamps at the beginning of any nested block. H:MM:SS

You can use the ctrl+shift+y hotkey (currently assumes you have Roam42 installed, let me know if you'd prefer I drop that requirement) to grab the current timestamp of the video you're cursor is nested under and add it to the beginning of your block.

window.players = {}
scanYouTube = (event, observer) => {
event.forEach((mutation) => {
if (mutation.type == "childList") {
mutation.addedNodes.forEach((node) => {
if (node.nodeType == Node.ELEMENT_NODE) {
node.querySelectorAll("iframe[src*='youtube.com']:not([src*='enablejsapi'])").forEach( (element)=>{
switchYouTube(element);
})
}
})
}
})
}
switchYouTube = (element) => {
const pathParts = new URL(element.src).pathname.split("/");
const youtubeid = pathParts[pathParts.length - 1];
const blockContainer = element.closest('.roam-block-container');
const block = element.closest('.roam-block');
if (!block) { return; }
const parent = element.parentElement;
console.log(element);
console.log(block);
full_id = block.id + "-" + youtubeid;
blockContainer.dataset.full_id = full_id;
blockContainer.dataset.youtube = true;
parent.id = full_id;
element.remove();
const player = new window.YT.Player(parent.id, {
height: '300', width: '450', videoId: youtubeid
});
const iframe = player.getIframe();
players[full_id] = player;
iframe.dataset.youtubeid = youtubeid
console.log("Found Youtube ID: " + youtubeid );
setTimeout(()=>{
addTimestampControls(blockContainer, block.id, player)
}, 200);
height = 300;
width = 450;
buttonDiv = document.createElement('div');
buttonDiv.dataset.youtubebuttons=true
for (i of [1, 1.2,1.5,2,2.5,3]) {
console.log(i);
const multiplier = i;
const button = document.createElement('button');
button.innerText = multiplier.toString();
button.addEventListener('click', () => {
console.log("Setting height: " + multiplier * height + "; width: " + multiplier * width);
iframe.height = multiplier * height;
iframe.width = multiplier * width;
});
button.style.marginRight = '8px';
buttonDiv.appendChild(button);
}
block.parentElement.parentElement.appendChild(buttonDiv);
}
addTimestampControls = (blockContainer, blockID, player) => {
const getTimestampAddButton = (event, observer) => {
if (Array.from(event[0].removedNodes).some((el) => { return el.id == blockID})) {
console.log("block was removed: " + blockID);
observer.disconnect();
removeButtons();
} else {
console.log(event);
addButtons();
}
}
const removeButtons = () => {
blockContainer.querySelectorAll(":scope .roam-block-container .roam-block[data-youtubets]").forEach(child => {
child.parentElement.querySelector('button[data-youtubets="' + child.dataset.youtubets + '"]').remove();
delete child.dataset.youtubets;
});
blockContainer.querySelector(":scope div[data-youtubebuttons=true]").remove();
}
const addButtons = () => {
blockContainer.querySelectorAll(":scope .roam-block-container .roam-block:not([data-youtubets])").forEach(child => {
const timestamp = getTimestamp(child);
if (timestamp != null) {
child.dataset.iframe_id = player.getIframe().id;
child.dataset.youtubets = timestamp;
addControlButton(child, timestamp, () => {
player.seekTo(timestamp, true)
pState = player.getPlayerState();
if (pState == -1 || pState == 0 || pState == 2) {
player.playVideo();
}
});
}
});
}
addButtons();
const x = new MutationObserver(getTimestampAddButton);
x.observe(blockContainer, {
childList: true,
subtree: true
});
};
getTimestamp = (block) => {
const matches = block.textContent.match(/^((?:\d+:)?\d+:\d\d)($|\D)/);
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;
}
addControlButton = (block, timestamp, fn) => {
const button = document.createElement('button');
button.innerText = '►';
button.addEventListener('click', fn);
button.style.marginRight = '8px';
button.dataset.youtubets = timestamp;
const printElement = (e) => {
if (Array.from(e[0].removedNodes).some((el) => { return el.id == block.id})) {
console.log("Removed Nodes: ");
console.log(e[0].removedNodes);
button.remove();
}
}
const x = new MutationObserver(printElement);
x.observe(block.parentElement, { childList: true });
block.parentElement.insertBefore(button, block);
};
if (document.querySelectorAll("html > script[src*='https://www.youtube.com/iframe_api']").length == 0) {
console.log("Load Youtube API");
const tag = document.createElement('script');
tag.src = 'https://www.youtube.com/iframe_api';
const htmlEl = document.getElementsByTagName('html')[0];
htmlEl.appendChild(tag);
}
if (document.querySelectorAll("head > script[src='https://craig.global.ssl.fastly.net/js/mousetrap/mousetrap.min.js?a4098']").length == 0) {
console.log("Installing Mousetrap");
const tag = document.createElement('script');
tag.src = 'https://craig.global.ssl.fastly.net/js/mousetrap/mousetrap.min.js?a4098';
const htmlEl = document.getElementsByTagName('head')[0];
htmlEl.appendChild(tag);
}
window.onYouTubeIframeAPIReady = () => {
document.querySelectorAll("iframe[src*='youtube.com']:not([src*='enablejsapi'])").forEach( (element)=>{
switchYouTube(element);
})
const observerYouTube = new MutationObserver(scanYouTube);
observerYouTube.observe(document.querySelector('.roam-body-main'), {
childList: true,
subtree: true
});
}
setTimeout(()=>{
// Add bindGlobal
(function(a){var c={},d=a.prototype.stopCallback;a.prototype.stopCallback=function(e,b,a,f){return this.paused?!0:c[a]||c[f]?!1:d.call(this,e,b,a)};a.prototype.bindGlobal=function(a,b,d){this.bind(a,b,d);if(a instanceof Array)for(b=0;b<a.length;b++)c[a[b]]=!0;else c[a]=!0};a.init()})(Mousetrap);
console.log("Set up hotkey");
Mousetrap.bindGlobal('ctrl+shift+y', (event, handler) => {
console.log("Getting timestamp");
event.preventDefault()
if (event.srcElement.localName == "textarea") {
bc = event.srcElement.closest('.roam-block-container[data-youtube=true]');
player = players[bc.dataset.full_id];
var timeStr = new Date(player.getCurrentTime() * 1000).toISOString().substr(11, 8)
console.log(timeStr);
var newValue = timeStr + " " + event.srcElement.innerHTML;
document.getElementById(event.srcElement.id).value = newValue;
return false;
}
});
},1000);
@abhayprasanna
Copy link

Issue: 1.2, 1.5, 2x sizing buttons duplicate every time you click and out of the embedded YT block
Enhancement request: Could Ctrl+Shift+Y minify the HH:MM:SS syntax so if it's a 3 minute video I only see 1:38 for example?

@CherryC99
Copy link

It seems not working anymore...

@joshirakshit30
Copy link

Please help me. I've installed the plugin and yet the hotkey doesn't seem to work.

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