Skip to content

Instantly share code, notes, and snippets.

@ewerybody
Last active March 6, 2026 16:24
Show Gist options
  • Select an option

  • Save ewerybody/f926e9c4c76d9a6c96f3b8c60ba7e391 to your computer and use it in GitHub Desktop.

Select an option

Save ewerybody/f926e9c4c76d9a6c96f3b8c60ba7e391 to your computer and use it in GitHub Desktop.
Bandcamp Inline player layout tweak, trackbar clickable, space to toggle play.
// ==UserScript==
// @name Bandcamp Player Tweaks
// @namespace https://github.com/ewerybody/
// @version 1.14.9
// @description Inline player layout fix, waveform seek, settings panel.
// @author ewerybody
// @match https://*.bandcamp.com/*
// @match https://bandcamp.com/*
// @grant none
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
const DEFAULTS = {
bg: '#222', fg: '#ccc', accent: '#f0f',
play_head_width: 2, prog_bar_height: 20,
show_tooltip: true, space_bar_play: true,
};
const BORDER_RADIUS = 2;
const POLL_MS = 500;
const COOKIE_KEY = 'bc-player-tweaks-cfg';
const pre_style = document.createElement('style');
pre_style.textContent = '.inline_player table { visibility: hidden; }';
document.head.appendChild(pre_style);
function load_settings() {
try {
const match = document.cookie.match(new RegExp(`(?:^|;\\s*)${COOKIE_KEY}=([^;]*)`));
return Object.assign({}, DEFAULTS, match ? JSON.parse(decodeURIComponent(match[1])) : {});
} catch { return Object.assign({}, DEFAULTS); }
}
function save_settings(cfg) {
try {
const val = encodeURIComponent(JSON.stringify(cfg));
const exp = new Date(Date.now() + 365 * 864e5).toUTCString(); // 1 year
document.cookie = `${COOKIE_KEY}=${val}; domain=.bandcamp.com; path=/; expires=${exp}; SameSite=Lax`;
} catch { }
}
function wait_for(selector, cb, interval = POLL_MS, maxTries = 40) {
let tries = 0;
const t = setInterval(() => {
const el = document.querySelector(selector);
if (el) { clearInterval(t); cb(el); }
else if (++tries >= maxTries) { clearInterval(t); }
}, interval);
}
function get_page_colors() {
const pgBd = document.getElementById('pgBd');
if (pgBd) {
const cs = getComputedStyle(pgBd);
const bg = css_color_to_hex(cs.backgroundColor);
const fg = css_color_to_hex(cs.color);
const accentEl = document.querySelector('.custom-link-color, #trackInfo a, .tralbum-tags a');
const accent = accentEl ? css_color_to_hex(getComputedStyle(accentEl).color) : fg;
if (bg && fg) return { bg, fg, accent: accent ?? fg };
}
return { bg: DEFAULTS.bg, fg: DEFAULTS.fg, accent: DEFAULTS.accent };
}
function css_color_to_hex(color) {
const m = color.match(/(\d+),\s*(\d+),\s*(\d+)/);
if (!m) return null;
return '#' + [m[1], m[2], m[3]].map(n => parseInt(n).toString(16).padStart(2, '0')).join('');
}
const cfg = load_settings();
wait_for('audio', audio => init(audio, cfg));
wait_for('#track_table', hoist_track_table);
function apply_styles(style_element, cfg) {
const pcs = get_page_colors();
style_element.textContent = `
.inline_player {
padding: 0 !important;
margin: 1em 0 0 0 !important;
position: relative !important;
}
#bc-controls-row {
display: flex;
align-items: center;
gap: 2px; padding: 4px 0px;
}
#bc-controls-row .playbutton { flex-shrink: 0; }
#bc-info-wrap {
flex: 1; display: flex; flex-direction: column;
min-width: 0; padding: 0 6px;
justify-content: center; overflow: hidden;
}
#bc-info-wrap .title,
#bc-info-wrap .track-title {
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important; font-weight: 600 !important;
}
#bc-info-wrap .time,
#bc-info-wrap .time-control {
font-size: 11px !important; opacity: 0.65 !important;
font-variant-numeric: tabular-nums !important;
}
#bc-inline-cfg-btn {
flex-shrink: 0;
color: ${pcs.accent}; background: none; border: none; cursor: pointer;
font-size: 14px; line-height: 1; padding: 0 4px;
opacity: 1.0; transition: opacity 0.15s;
}
#bc-inline-cfg-btn:hover { opacity: 1; }
#bc-controls-row .prevbutton,
#bc-controls-row .nextbutton { flex-shrink: 0; }
#bc-seek-row { width: 100%; padding: 4px 0; }
#bc-seek-row .progbar_empty,
#bc-seek-row .progbar {
cursor: pointer !important;
display: block !important;
width: 100% !important; height: ${cfg.prog_bar_height}px !important;
overflow: visible !important;
margin: 0 !important; box-sizing: border-box !important;
border-radius: ${BORDER_RADIUS}px;
}
#bc-seek-row .progbar .thumb {
width: ${cfg.play_head_width}px !important;
height: ${cfg.prog_bar_height}px !important;
top: -4px; padding: 6px 0 0 0 !important;
min-width: unset !important; max-width: unset !important;
min-height: unset !important; max-height: unset !important;
margin-left: -${Math.floor(cfg.play_head_width / 2)}px !important;
border: none !important;
box-sizing: content-box !important;
background: ${pcs.accent} !important;
opacity: 0.9 !important;
box-shadow: none !important;
}
#bc-seek-tooltip {
position: fixed; background: ${pcs.fg}; color: ${pcs.bg};
font-size: 11px; font-family: 'Helvetica Neue', Arial, sans-serif;
padding: 2px 6px; border-radius: ${BORDER_RADIUS}px; pointer-events: none;
z-index: 99998; display: none; transform: translate(-50%, -130%);
}
#bc-settings-panel {
position: absolute; z-index: 999999; right: 0; top: 100%;
background: ${pcs.bg}; color: ${pcs.fg};
font-family: 'Helvetica Neue', Arial, sans-serif; font-size: 13px;
border-radius: ${BORDER_RADIUS}px; padding: 20px 24px; width: 300px;
box-shadow: 0 6px 24px ${pcs.fg}80, 0 0 0 1px ${pcs.fg}80;
}
#bc-settings-panel h2 { margin: 0 0 4px; font-size: 14px; font-weight: 700; color: ${pcs.fg}; }
.bc-settings-hint { font-size: 11px; opacity: 0.4; margin-bottom: 16px; line-height: 1.5; color: ${pcs.fg}}
.bc-setting-row {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 12px; gap: 10px;
}
.bc-setting-row label { flex: 1; opacity: 0.85; color: ${pcs.fg}; }
.bc-setting-row label.bc-muted { opacity: 0.35; }
.bc-setting-row input[type=number] {
width: 58px; background: #2e2e2e; border: 1px solid #444;
border-radius: ${BORDER_RADIUS}px; color: #eee; padding: 3px 6px; font-size: 13px; flex-shrink: 0;
}
.bc-setting-row input[type=checkbox] {
width: 15px; height: 15px; cursor: pointer; accent-color: ${pcs.accent}; flex-shrink: 0;
}
.bc-settings-buttons { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
.bc-settings-buttons button {
padding: 5px 14px; border-radius: ${BORDER_RADIUS}px; border: none;
cursor: pointer; font-size: 12px; font-weight: 600;
}
#bc-settings-save { background: ${pcs.accent}; color: ${pcs.bg}; }
#bc-settings-cancel { background: #333; color: #aaa; }
#bc-settings-reset { background: none; color: #555; border: 1px solid #333 !important;
margin-right: auto; font-weight: 400 !important; }
`;
}
function init(audio, cfg) {
const style = document.createElement('style');
style.id = 'bc-player-tweaks-styles';
document.head.appendChild(style);
apply_styles(style, cfg);
const tooltip = document.createElement('div');
tooltip.id = 'bc-seek-tooltip';
document.body.appendChild(tooltip);
function show_tooltip(e, secs) {
if (!cfg.show_tooltip) return;
tooltip.textContent = format_time(secs);
tooltip.style.left = e.clientX + 'px';
tooltip.style.top = e.clientY + 'px';
tooltip.style.display = 'block';
}
function hide_tooltip() { tooltip.style.display = 'none'; }
// restructure inline player DOM
// Hide BC's table, pull out known elements into two clean wrapper divs.
wait_for('.inline_player', inlinePlayer => {
const table = inlinePlayer.querySelector('table');
if (!table) return;
const playBtn = inlinePlayer.querySelector('.playbutton');
const prevBtn = inlinePlayer.querySelector('.prevbutton');
const nextBtn = inlinePlayer.querySelector('.nextbutton');
const progbar = inlinePlayer.querySelector('.progbar, .progbar_empty');
// find info td — the one that contains neither buttons nor progbar
let infoTd = null;
for (const td of inlinePlayer.querySelectorAll('td')) {
if (!td.contains(playBtn) && !td.contains(prevBtn) &&
!td.contains(nextBtn) && !td.contains(progbar)) {
infoTd = td; break;
}
}
const ctrlRow = document.createElement('div');
ctrlRow.id = 'bc-controls-row';
if (playBtn) ctrlRow.appendChild(playBtn);
const infoWrap = document.createElement('div');
infoWrap.id = 'bc-info-wrap';
if (infoTd) while (infoTd.firstChild) infoWrap.appendChild(infoTd.firstChild);
ctrlRow.appendChild(infoWrap);
const cfgBtn = document.createElement('button');
cfgBtn.id = 'bc-inline-cfg-btn';
cfgBtn.textContent = '⛭';
cfgBtn.title = 'Player settings';
cfgBtn.addEventListener('click', e => open_settings_panel(cfg, style, e.currentTarget));
ctrlRow.appendChild(cfgBtn);
if (prevBtn) ctrlRow.appendChild(prevBtn);
if (nextBtn) ctrlRow.appendChild(nextBtn);
const seekRow = document.createElement('div');
seekRow.id = 'bc-seek-row';
if (progbar) seekRow.appendChild(progbar);
// hide BC's table, append our rows
table.style.display = 'none';
inlinePlayer.appendChild(ctrlRow);
inlinePlayer.appendChild(seekRow);
});
wait_for('#bc-seek-row .progbar, #bc-seek-row .progbar_empty', progbar => {
progbar.addEventListener('click', e => {
if (audio.duration) audio.currentTime = frac_of(e, progbar) * audio.duration;
if (audio.paused) document.querySelector('.playbutton')?.click();
}, true);
progbar.addEventListener('mousemove', e => { if (audio.duration) show_tooltip(e, frac_of(e, progbar) * audio.duration); });
progbar.addEventListener('mouseleave', hide_tooltip);
});
pre_style.remove();
// register space for play toggle
document.addEventListener('keydown', e => {
if (!cfg.space_bar_play) return;
if (e.key !== ' ') return;
const tag = document.activeElement?.tagName;
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
e.preventDefault();
document.querySelector('.playbutton')?.click();
});
} // end of init()
function open_settings_panel(cfg, style_element, anchor) {
const existing = document.getElementById('bc-settings-panel');
if (existing) { existing.remove(); return; }
const panel = document.createElement('div');
panel.id = 'bc-settings-panel';
panel.innerHTML = `
<h2>Player Tweaks</h2>
<p class="bc-settings-hint">Changes preview live. Save to persist.</p>
<div class="bc-setting-row">
<label>Playhead width (px)</label>
<input type="number" id="bcs-play-head-width" value="${cfg.play_head_width}" min="1" max="12">
</div>
<div class="bc-setting-row">
<label>Seek bar height (px)</label>
<input type="number" id="bcs-prog-bar-height" value="${cfg.prog_bar_height}" min="4" max="48">
</div>
<div class="bc-setting-row">
<label>Show seek tooltip</label>
<input type="checkbox" id="bcs-show-tooltip" ${cfg.show_tooltip ? 'checked' : ''}>
</div>
<div class="bc-setting-row">
<label>Toggle Play/Pause on Space</label>
<input type="checkbox" id="bcs-space-bar-play" ${cfg.space_bar_play ? 'checked' : ''}>
</div>
<div class="bc-settings-buttons">
<button id="bc-settings-reset">Reset</button>
<button id="bc-settings-cancel">Cancel</button>
<button id="bc-settings-save">Save</button>
</div>
`;
const player = document.querySelector('.inline_player');
player.appendChild(panel);
function read_panel() {
return {
play_head_width: parseInt(document.getElementById('bcs-play-head-width').value, 10) || 2,
prog_bar_height: parseInt(document.getElementById('bcs-prog-bar-height').value, 10) || 12,
show_tooltip: document.getElementById('bcs-show-tooltip').checked,
space_bar_play: document.getElementById('bcs-space-bar-play').checked,
};
}
function close_cancel() { apply_styles(style_element, cfg); panel.remove(); }
panel.addEventListener('input', () => {
const next = read_panel();
cfg.show_tooltip = next.show_tooltip;
apply_styles(style_element, next);
});
function on_key_down(e) {
if (e.key === 'Escape') {
close_cancel();
document.removeEventListener('keydown', on_key_down);
}
}
document.addEventListener('keydown', on_key_down);
function on_outside_click(e) {
if (!panel.contains(e.target) && e.target !== anchor) {
close_cancel();
document.removeEventListener('click', on_outside_click, true);
}
}
setTimeout(() => document.addEventListener('click', on_outside_click, true), 0);
document.getElementById('bc-settings-save').addEventListener('click', () => {
const next = read_panel();
Object.assign(cfg, next);
save_settings(cfg);
cleanup();
panel.remove();
});
document.getElementById('bc-settings-cancel').addEventListener('click', () => {
cleanup();
close_cancel();
});
document.getElementById('bc-settings-reset').addEventListener('click', () => {
document.getElementById('bcs-play-head-width').value = DEFAULTS.play_head_width;
document.getElementById('bcs-prog-bar-height').value = DEFAULTS.prog_bar_height;
document.getElementById('bcs-show-tooltip').checked = DEFAULTS.show_tooltip;
document.getElementById('bcs-space-bar-play').checked = DEFAULTS.space_bar_play;
apply_styles(style_element, read_panel());
});
function cleanup() {
document.removeEventListener('click', on_outside_click, true);
document.removeEventListener('keydown', on_key_down);
}
}
// move track table right under the inline player
function hoist_track_table(table) {
const player = document.querySelector('.inline_player');
if (!player) return;
if (player.nextElementSibling === table) return;
player.insertAdjacentElement('afterend', table);
}
function frac_of(e, el) {
const rect = el.getBoundingClientRect();
return Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
}
function format_time(secs) {
if (!isFinite(secs)) return '0:00';
const m = Math.floor(secs / 60);
const s = Math.floor(secs % 60);
return `${m}:${s.toString().padStart(2, '0')}`;
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment