Last active
September 21, 2025 14:58
-
-
Save rxliuli/a5aec5edd7070b7f38112665567931bf to your computer and use it in GitHub Desktop.
Keep YouTube Music playing in background
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 YouTube Music Background Play | |
// @namespace https://rxliuli.com | |
// @version 0.1.1 | |
// @description Keep YouTube Music playing in background | |
// @match https://music.youtube.com/* | |
// @sandbox DOM | |
// @grant none | |
// @run-at document-start | |
// ==/UserScript== | |
(function () { | |
'use strict'; | |
class Vista { | |
constructor(interceptors = []) { | |
this.interceptors = interceptors; | |
} | |
middlewares = []; | |
cancels = []; | |
use(middleware) { | |
this.middlewares.push(middleware); | |
return this; | |
} | |
intercept() { | |
this.cancels = this.interceptors.map( | |
(interceptor) => interceptor(this.middlewares) | |
); | |
} | |
destroy() { | |
this.cancels.forEach((cancel) => cancel()); | |
this.cancels = []; | |
} | |
} | |
async function handleRequest(context, middlewares) { | |
const compose = (i) => { | |
if (i >= middlewares.length) { | |
return Promise.resolve(); | |
} | |
return middlewares[i](context, () => compose(i + 1)); | |
}; | |
await compose(0); | |
} | |
const interceptEvent = (middlewares, options) => { | |
const originalAddEventListener = EventTarget.prototype.addEventListener; | |
const originalRemoveEventListener = EventTarget.prototype.removeEventListener; | |
const wrappedListeners = options?.wrappedListeners ?? []; | |
EventTarget.prototype.addEventListener = function(type, listener, options2) { | |
if (!listener) { | |
console.warn("Event listener is null or undefined"); | |
return; | |
} | |
const wrappedListener = function(event) { | |
return handleRequest({ type: "event", event }, [ | |
...middlewares, | |
(c) => typeof listener === "function" ? listener?.(c.event) : listener?.handleEvent(c.event) | |
]); | |
}; | |
wrappedListeners.push({ | |
original: listener, | |
dom: this, | |
listener: wrappedListener, | |
type, | |
options: options2 | |
}); | |
return originalAddEventListener.call(this, type, wrappedListener, options2); | |
}; | |
EventTarget.prototype.removeEventListener = function(type, listener, options2) { | |
if (!listener) { | |
console.warn("Event listener is null or undefined"); | |
return; | |
} | |
const listeners = wrappedListeners.filter( | |
(it) => it.dom === this && it.type === type && it.original === listener && it.options === options2 | |
); | |
if (listeners.length === 0) { | |
console.warn("Event listener is not wrapped, cannot remove listener"); | |
return; | |
} | |
listeners.forEach((it) => { | |
originalRemoveEventListener.call(this, type, it.listener, options2); | |
wrappedListeners.splice(wrappedListeners.indexOf(it), 1); | |
}); | |
}; | |
return () => { | |
wrappedListeners.forEach((it) => { | |
originalRemoveEventListener.call(it.dom, it.type, it.listener, it.options); | |
originalAddEventListener.call(it.dom, it.type, it.original, it.options); | |
}); | |
wrappedListeners.length = 0; | |
EventTarget.prototype.addEventListener = originalAddEventListener; | |
EventTarget.prototype.removeEventListener = originalRemoveEventListener; | |
}; | |
}; | |
function nonStop() { | |
Object.defineProperty(document, "hidden", { | |
get: () => false, | |
configurable: true | |
}); | |
Object.defineProperty(document, "visibilityState", { | |
get: () => "visible", | |
configurable: true | |
}); | |
document.hasFocus = () => true; | |
const observer = new MutationObserver((mutations) => { | |
mutations.forEach((mutation) => { | |
mutation.addedNodes.forEach((node) => { | |
if (node.nodeName === "YTMUSIC-YOU-THERE-RENDERER" || node instanceof HTMLElement && node.querySelector("ytmusic-you-there-renderer")) { | |
console.log('Auto-closing "Are you there?" dialog'); | |
setTimeout(() => { | |
const yesButton = document.querySelector( | |
'ytmusic-you-there-renderer button[aria-label="Yes"]' | |
); | |
if (yesButton instanceof HTMLButtonElement) { | |
yesButton.click(); | |
console.log('Clicked "Yes" button'); | |
} | |
}, 100); | |
} | |
}); | |
}); | |
}); | |
observer.observe(document.body, { | |
childList: true, | |
subtree: true | |
}); | |
new Vista([interceptEvent]).use(async (c, next) => { | |
if (c.event.target instanceof HTMLVideoElement || c.event.target instanceof HTMLAudioElement || c.event.target instanceof HTMLImageElement || c.event.target instanceof XMLHttpRequest || c.event.target instanceof IDBRequest || c.event.target instanceof IDBTransaction || c.event.target instanceof SourceBuffer) { | |
return await next(); | |
} | |
if (c.event.target === window || c.event.target === document) { | |
if ([ | |
"focus", | |
"focusin", | |
"pageshow", | |
"visibilitychange", | |
"mouseenter", | |
"mouseover", | |
"mousemove" | |
].includes(c.event.type)) { | |
console.log("Blocked event listener:", c.event.target, c.event.type); | |
return; | |
} | |
} | |
if (![ | |
"mousemove", | |
"mouseover", | |
"mouseout", | |
"pointermove", | |
"pointerout", | |
"pointerover" | |
].includes(c.event.type)) { | |
console.log("Event:", c.event.type, c.event.target); | |
} | |
await next(); | |
}).intercept(); | |
} | |
nonStop(); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment