Last active
November 18, 2023 07:59
-
-
Save solarshado/1273d2239aaee91479f6d1bdb92a1703 to your computer and use it in GitHub Desktop.
time remaining for youtube taking playbackRate into account
This file contains 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
Youtube Progress Display |
This file contains 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
"use strict"; | |
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) { | |
if(typeof message === 'object' && 'showPageAction' in message) { | |
const show = !!message.showPageAction; | |
chrome.pageAction[show ? "show" : "hide"](sender.tab.id); | |
} | |
sendResponse(); | |
}); | |
chrome.pageAction.onClicked.addListener(function(tab) { | |
chrome.tabs.sendMessage(tab.id, {doThing: true}); | |
}); |
This file contains 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
"use strict"; | |
const MODE = ( | |
window.location.host.includes("youtube.com") ? "YT" : | |
window.location.host.includes("nebula.app") ? "N" : | |
window.location.host.includes("nebula.tv") ? "N" : | |
"YT" // fallback | |
); | |
/** | |
* @template T | |
* @param {{[key in MODE]: T}} map | |
*/ | |
const ms = (map) => map[MODE]; // helper for constructing MODE_HELPER | |
const MODE_HELPER = { | |
mode: MODE, | |
/** @type {(display:HTMLElement) => boolean} */ | |
addDisplay: ms({ | |
"YT": (display) => { | |
const container = document.querySelector("div.ytp-left-controls div.ytp-time-display.notranslate"); | |
if(!container) | |
return false; | |
container.appendChild(display); | |
return true; | |
}, | |
"N": (display) => { | |
const container = document.querySelector("#video-controls .icon-spacing")?.lastChild; | |
if(!container) | |
return false; | |
// steal a css class to get the font right | |
display.classList.add(container.firstChild.classList); | |
// margin adjustment for personal taste | |
display.style.marginLeft = "0.4em"; | |
container.appendChild(display); | |
return true; | |
} | |
}), | |
findVideo: ms({ | |
"YT": () => { | |
const videos = Array.from(document.getElementsByTagName("video")); | |
// exclude videos within a 'class="ad-interrupting"' container | |
// https://stackoverflow.com/a/60537660/ | |
const noYTFuckery = videos.filter(el=>!el.closest(".ad-interrupting")) | |
// if we're too early, the bad vid might not be in that | |
// div yet... so try again later... gross but works | |
if(noYTFuckery.length > 1) return null; | |
return noYTFuckery[0]; | |
}, | |
"N": () => { | |
return document.getElementsByTagName("video")[0]; | |
} | |
}), | |
retryAfterVideoNotFound: ms({ | |
"YT": () => { | |
// tactic found here: https://stackoverflow.com/a/34100952 | |
document.addEventListener("yt-navigate-finish",tryCreateAndWireUpDisplay, {once: true, passive: true}); | |
// above no longer working consistently, so: | |
// lazy option; kinda gross, but works | |
setTimeout(tryCreateAndWireUpDisplay, 100); | |
}, | |
"N": () => { | |
// try to only run on video pages | |
if(!window.location.pathname.includes("/videos/")) | |
return; | |
// lazy option; kinda gross, but works | |
setTimeout(tryCreateAndWireUpDisplay, 100); | |
} | |
}), | |
}; | |
// temp hack, I hope | |
function messageListener(msg, sender, sendResponse) { | |
chrome.runtime.onMessage.removeListener(messageListener); | |
tryCreateAndWireUpDisplay(); | |
sendResponse(); | |
} | |
function queueRetry() { | |
MODE_HELPER.retryAfterVideoNotFound(); | |
// temp hack, I hope | |
chrome.runtime.onMessage.addListener(messageListener); | |
chrome.runtime.sendMessage({showPageAction: true}); | |
} | |
// main entry-point | |
function tryCreateAndWireUpDisplay() { | |
const newId = "customTimeDisplay"; | |
const video = MODE_HELPER.findVideo(); | |
if(!video) { // not on a video page, or page not done loading | |
queueRetry(); | |
return; | |
} | |
// temp hack, I hope | |
chrome.runtime.sendMessage({showPageAction: false}); | |
const newElem =(function getOrBuild(id) { | |
let r = document.getElementById(id); | |
if(!r){ | |
r = document.createElement("span"); | |
r.id = id; | |
r.innerText = "awaiting event to update"; | |
const added = MODE_HELPER.addDisplay(r); | |
if(!added) { // page not done loaded? hopefully that's it. should probably try to confirm | |
return null; | |
} | |
for(const event of ["click","auxclick","contextmenu"]) | |
r.addEventListener(event, clickListener) | |
for(const event of ["timeupdate","ratechange"]) | |
video.addEventListener(event, listener) | |
} | |
return r; | |
})(newId); | |
if(newElem === null) { | |
queueRetry(); | |
return; | |
} | |
function listener(evt) { | |
newElem.innerText = buildText(video); | |
} | |
//const speeds = [1,1.25,1.5,1.75,2,2.5,3]; | |
const speeds = [1,1.5,1.75,2,2.5]; | |
function speed(i) { return speeds[Math.min(speeds.length-1,Math.max(0,i))]; } | |
function clickListener(evt) { | |
evt.preventDefault(); evt.stopImmediatePropagation(); | |
if(evt.type === "contextmenu") return; | |
const faster = (evt.button === 0); | |
const curSpeed = video.playbackRate; | |
const curIndex = speeds.indexOf(curSpeed); | |
const curSpeedInList = curIndex !== -1; | |
const newSpeed = speed( | |
curSpeedInList ? | |
curIndex + (faster ? 1 : -1) : | |
(faster ? 2 : 1) ); | |
// this alone works, but desyncs youtube's *idea* of the playback rate | |
// from the actual rate, causing < and > to possibly behave unexpectedly | |
video.playbackRate = newSpeed; | |
if(MODE === "YT") { | |
// tactic found here: https://stackoverflow.com/a/9517879 | |
// (running the same code here results in a `player` missing those funcs, | |
// plus setPlaybackRate() ignores non-standard rates) | |
(function inject(actualCode){ | |
const script = document.createElement('script'); | |
script.textContent = `(function(){${actualCode}})()`; | |
(document.head||document.documentElement).appendChild(script); | |
script.remove(); | |
})(` | |
const player = document.querySelector("#movie_player"); | |
player.setPlaybackRate(${newSpeed}); | |
`); | |
} | |
} | |
function buildText(video) { | |
const now = video.currentTime; | |
const end = video.duration; | |
const rate = video.playbackRate; | |
const n = (end-now)/rate; | |
const t = (!isNaN(n) && n >= 0) ? | |
new Date(n * 1000).toISOString().substring( ( n>3600 ? 11 : 14 ), 19) : | |
"??:??"; | |
return ` -- ${t} left @ ${rate}x`; | |
} | |
} | |
tryCreateAndWireUpDisplay(); | |
/* vim: set et sw=0 tabstop=2: */ |
This file contains 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 YT progress display | |
// @version 1 | |
// @grant none | |
// @match *://*.youtube.com/* | |
// @match *://*.nebula.app/* | |
// @require https://gist.githubusercontent.com/solarshado/1273d2239aaee91479f6d1bdb92a1703/raw/contentScript.js | |
// ==/UserScript== |
This file contains 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
{ | |
"name": "Youtube Progress Display", | |
"version": "0.1", | |
"description": "Shows YT time remaining considering playback rate", | |
"permissions": [ | |
"*://*.youtube.com/*", | |
"*://*.nebula.app/*" | |
], | |
"content_scripts": [{ | |
"matches": [ | |
"*://*.youtube.com/*", | |
"*://*.nebula.tv/*", | |
"*://*.nebula.app/*" | |
], | |
"js": ["contentScript.js"] | |
}], | |
"background": { | |
"scripts": ["background.js"], | |
"persistent": false | |
}, | |
"page_action": { | |
"default_title": "Force add timer" | |
}, | |
"manifest_version": 2 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment