-
-
Save andrewf76/3bf2cd0845f4e38fe8b6604d1d256697 to your computer and use it in GitHub Desktop.
Spotify ads skip with internal player expose
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 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 GM_addElement | |
| // @run-at document-idle | |
| // ==/UserScript== | |
| GM_addElement('script', { textContent: `(function () { | |
| const DEV = false; | |
| const log = (...a) => DEV && console.log('[SpotifyPlayer]', ...a); | |
| const warn = (...a) => DEV && console.warn('[SpotifyPlayer]', ...a); | |
| const MODULE_LIST_PLAYER_FALLBACK = 94275; | |
| 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)'); | |
| })();` }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment