Skip to content

Instantly share code, notes, and snippets.

@addavriance
Last active June 23, 2026 19:27
Show Gist options
  • Select an option

  • Save addavriance/085b2f04a4600ab65791dcd55493493f to your computer and use it in GitHub Desktop.

Select an option

Save addavriance/085b2f04a4600ab65791dcd55493493f to your computer and use it in GitHub Desktop.
Spotify ads skip with internal player expose
// ==UserScript==
// @name Spotify Ad Skipper + Seek Controls
// @name:ru Spotify пропуск рекламы + управление перемоткой
// @description Exposes Spotify internal player as window.SpotifyPlayer with seek, skip ads, and keyboard controls
// @description:ru Открывает внутренний плеер Spotify как window.SpotifyPlayer с перемоткой, пропуском рекламы и управлением с клавиатуры
// @keywords spotify skip ads, spotify ad skipper, spotify seek bar, spotify pip, picture in picture spotify, spotify premium bypass, spotify мини плеер, реклама спотифай, спотифай пип
// @namespace https://gist.github.com/addavriance
// @author addavriance
// @license MIT
// @version 1.4.0
// @match https://open.spotify.com/*
// @grant none
// @run-at document-idle
// ==/UserScript==
(function () {
const script = document.createElement('script');
script.textContent = `(${function () {
const DEV = false;
const log = (...a) => DEV && console.log('[SpotifyPlayer]', ...a);
const warn = (...a) => DEV && console.warn('[SpotifyPlayer]', ...a);
// Webpack module IDs
const MODULE_LIST_PLAYER_FALLBACK = 94275;
// Seek offsets (seconds)
const SEEK_FORWARD = 15;
const SEEK_BACKWARD = -15;
const chunkName = Object.keys(window).find(k => k.startsWith('rspackChunk') || k.startsWith('webpackChunk'));
const req = window[chunkName].push([[Symbol()], {}, r => r]);
function findModules(...terms) {
return Object.entries(req.m)
.filter(([, f]) => terms.every(t => f.toString().includes(t)))
.map(([id, f]) => ({ id, preview: f.toString().slice(0, 120) }));
}
function resolveListPlayerClass() {
for (const { id } of findModules('allowSeeking', '_loadedList', 'LIST_PLAYER_NO_LIST')) {
try {
const cls = Object.values(req(id)).find(v =>
typeof v === 'function' &&
v.prototype?.seek &&
v.prototype?.load &&
v.prototype?._getTrackPlayer
);
if (cls) return cls;
} catch { /* skip */ }
}
warn('ListPlayerClass not found, falling back to default module', MODULE_LIST_PLAYER_FALLBACK);
return req(MODULE_LIST_PLAYER_FALLBACK).is;
}
const ListPlayerClass = resolveListPlayerClass();
const origLoad = ListPlayerClass.prototype.load;
ListPlayerClass.prototype.load = function (list) {
const uri = list?._tracks?.[0]?.uri ?? list?.tracks?.[0]?.uri ?? '';
window[uri.includes('spotify:canvas:') || uri.includes('spotify:ad:')
? '_canvasPlayer'
: '_listPlayer'
] = this;
return origLoad.apply(this, arguments);
};
const sp = window.SpotifyPlayer = {
currentPosition: () => 0,
_adSkipping: () => false,
_lp: () => window._listPlayer ?? null,
_ll() { return this._lp()?._loadedList ?? null; },
getPosition() { return this.currentPosition },
updatePosition(newPos) { this.currentPosition = newPos },
getDuration() { return this._ll()?._currentState?.duration ?? 0; },
getTrack() { return this._lp()?._currentTrack ?? null; },
isAd() { return this.getTrack()?.contentType === 'ad'; },
seek(seconds) {
const lp = this._lp(), ll = this._ll();
if (!lp || !ll) return warn('player not ready');
ll.allowSeeking = () => true;
const pos = Math.max(0, this.getPosition() * 1000 + seconds * 1000);
lp.seek(pos).catch(e => warn('seek error:', e.message));
},
seekTo(ms) {
const lp = this._lp(), ll = this._ll();
if (!lp || !ll) return warn('player not ready');
ll.allowSeeking = () => true;
const pos = Math.max(0, ms);
lp.seek(pos)
.then(() => { if (lp._currentTrackOptions) lp._currentTrackOptions.position = pos; })
.catch(e => warn('seekTo error:', e.message));
},
skipAd() {
const lp = this._lp();
if (!lp || !this.isAd() || this._adSkipping()) return;
_adSkipping = true;
log('skipping ad via next()');
lp.next('trackdone')
.catch(e => warn('skipAd error:', e.message))
.finally(() => { _adSkipping = false; });
},
findModules,
};
setInterval(() => {
if (sp.isAd()) {
sp.skipAd();
return;
}
const lp = sp._lp();
if (!lp) return;
Promise.race([
lp._getTrackPlayer(),
new Promise((_, rej) => setTimeout(() => rej('timeout'), 200))
])
.then(tp => sp.updatePosition(tp._player.currentTime))
.catch(() => {});
}, 50);
document.addEventListener('keydown', e => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
if (e.key === 'ArrowRight') sp.seek(SEEK_FORWARD);
if (e.key === 'ArrowLeft') sp.seek(SEEK_BACKWARD);
});
log('ready - seek(±sec) | seekTo(ms) | skipAd() | isAd() | getTrack() | findModules(...terms)');
}})();`;
document.documentElement.appendChild(script);
script.remove();
})();
@andrewf76

Copy link
Copy Markdown

Made some tweaks to bypass the CSP errors when using Tampermonkey in Firefox

https://gist.github.com/andrewf76/3bf2cd0845f4e38fe8b6604d1d256697

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