Skip to content

Instantly share code, notes, and snippets.

@kugland
Last active February 9, 2024 05:20
  • Star 8 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save kugland/ab95feddd2d592c3de859bb589f06fdd to your computer and use it in GitHub Desktop.
Download YouTube subtitles (GreaseMonkey/TamperMonkey script)
// ==UserScript==
// @name Download YouTube subtitles
// @namespace http://tampermonkey.net/
// @version 0.1
// @description Now you can download YouTube subtitles
// @author André Kugland
// @match http*://*.youtube.com/*
// @grant none
// @require https://cdn.jsdelivr.net/npm/file-saver@2.0.2/dist/FileSaver.min.js#sha256=bbf27552b76b9379c260579fa68793320239be2535ba3083bb67d75e84898e18
// ==/UserScript==
(function() {
'use strict';
const buttonId = 'download-subtitles-userscript';
const parentSelector = 'ytd-transcript-footer-renderer div#label-text.yt-dropdown-menu';
const buttonSelector = '#' + buttonId;
function formatOffset(offset, sep) {
sep = sep || '.';
const millis = offset % 1000;
offset = Math.floor(offset / 1000);
const seconds = offset % 60;
offset = Math.floor(offset / 60);
const minutes = offset % 60;
const hours = Math.floor(offset / 60);
return (`${hours < 10 ? '0' : ''}${hours}:`
+ `${minutes < 10 ? '0' : ''}${minutes}:`
+ `${seconds < 10 ? '0' : ''}${seconds}${sep}`
+ `${millis < 100 ? '0' : ''}${millis < 10 ? '0' : ''}${millis}`);
}
function absorbEvent(event) {
event.preventDefault();
event.stopPropagation();
}
function getSubtitles() {
const cueElements = document.querySelectorAll('.cues > div[start-offset]');
const cues = Array.from(cueElements, elm => ({
startOffset: parseInt(elm.getAttribute('start-offset')),
text: elm.innerHTML.trim(),
}));
const result = cues.map(cue => `${formatOffset(cue.startOffset)} --> ${cue.text}`).join('\n');
return result;
}
function saveSubtitles(text) {
const blob = new Blob([text], {type: "text/plain;charset=utf-8"});
window.saveAs(blob, "subtitle.txt");
}
function createButton() {
if (document.querySelector(buttonSelector)) { return; }
const parentElement = document.querySelector(parentSelector);
if (!parentElement) { return; }
const button = document.createElement('div');
button.id = 'download-subtitles-userscript';
button.innerText = 'Download';
button.style.textTransform = 'uppercase';
button.style.display = 'inline-block';
button.style.fontSize = '80%';
button.style.margin = '0rem 1rem';
button.style.letterSpacing = '0.05rem';
button.style.cursor = 'pointer';
button.addEventListener('mousedown', absorbEvent);
button.addEventListener('click', function (event) {
absorbEvent(event);
saveSubtitles(getSubtitles());
});
parentElement.append(button);
}
// Select the node that will be observed for mutations
const targetNode = document.querySelector('#panels');
// Create an observer instance linked to the callback function
const observer = new MutationObserver((mutationsList, observer) => {
for (let mutation of mutationsList) {
if (mutation.target.tagName === 'IRON-DROPDOWN') {
createButton();
}
}
});
// Start observing the target node for configured mutations
observer.observe(targetNode, {
childList: true,
subtree: true,
});
})();
@benliddicott
Copy link

Minified file-saver is very small. May I suggest inlining it so there are no dependencies?

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