Last active
February 9, 2024 05:20
-
-
Save kugland/ab95feddd2d592c3de859bb589f06fdd to your computer and use it in GitHub Desktop.
Download YouTube subtitles (GreaseMonkey/TamperMonkey script)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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/[email protected]/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, | |
}); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Minified file-saver is very small. May I suggest inlining it so there are no dependencies?