您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
for Bandcamp and its embeded player: seek, autoseek when changing track, play/pause, go to prev/next release, volume control, pitch control. Designed for those digging through lots of tunes.
// ==UserScript== // @name BC: keyboard shortcuts // @description for Bandcamp and its embeded player: seek, autoseek when changing track, play/pause, go to prev/next release, volume control, pitch control. Designed for those digging through lots of tunes. // @namespace userscript1 // @version 1.4.6 // @grant GM.xmlHttpRequest // @connect bandcamp.com // @match https://bandcamp.com/* // @match https://*.bandcamp.com/* // @match https://*/* // @license GPLv3 // ==/UserScript== (function() { 'use strict'; // Key_ settings refer to physical positions as if you had a QWERTY layout, not letters. // https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values const playPause = 'KeyP'; const prev = 'KeyI'; const next = 'KeyO'; const prevAndSeek = 'KeyH'; const nextAndSeek = 'KeyL'; const seekBack = 'KeyJ'; const seekForward = 'KeyK'; const prevRelease = 'KeyY'; const nextRelease = 'KeyU'; const volDown = 'Minus'; const volUp = 'Equal'; const speedDown = 'BracketLeft'; const speedUp = 'BracketRight'; const speedReset = 'Semicolon'; const initialSeek = 60; const manualSeek = 30; // end configuration // only run on *bandcamp.com or bandcamp on a custom domain if (!window.location.hostname.endsWith('bandcamp.com') && !document.head.querySelector('meta[property="twitter:site"][content="@bandcamp"]') ) { return; } const aud = $('audio'); // if (!aud) { return; } // we want to allow next release on non-player pages var prevButton, nextButton, playButton; $('#customHeaderWrapper')?.insertAdjacentHTML('beforeEnd', `<div id="shortcuts-message" style="position:absolute; text-align: center; left: 0; right: 0;"></div>`); window.addEventListener('keydown', (evt) => { if (evt.altKey || evt.ctrlKey || evt.metaKey) { return; } if ($('.ui-widget-overlay') || document.activeElement.tagName == 'INPUT') { // don't run if dialog box is open or we're typing into a field return; } // check every time to allow collection page to work if (!findButtons() && (evt.code != nextRelease)) { return; } // console.log(evt.code); // uncomment to check key codes switch(evt.code) { case prev: prevButton.click(); scrollEmbedPlayer(); message(''); break; case prevAndSeek: prevButton.click(); aud.currentTime = initialSeek; scrollEmbedPlayer(); message(''); break; case next: nextButton.click(); scrollEmbedPlayer(); message(''); break; case nextAndSeek: nextButton.click(); aud.currentTime = initialSeek; scrollEmbedPlayer(); message(''); break; case seekBack: if (aud.paused) { playButton.click(); } aud.currentTime -= manualSeek; break; case seekForward: if (aud.paused) { playButton.click(); } aud.currentTime += manualSeek; break; case playPause: playButton.click(); if (playPause === 'Space') { evt.preventDefault(); // prevent page scroll } break; case nextRelease: aud.pause(); changeRelease(1); break; case prevRelease: aud.pause(); changeRelease(-1); break; case volDown: aud.volume = Math.max(aud.volume - 0.1, 0).toFixed(2); break; case volUp: aud.volume = Math.min(aud.volume + 0.1, 1).toFixed(2); break; case speedDown: aud.preservesPitch = false; aud.playbackRate = Math.max(aud.playbackRate - 0.02, 0.2).toFixed(2); message(aud.playbackRate); break; case speedUp: aud.preservesPitch = false; aud.playbackRate = Math.min(aud.playbackRate + 0.02, 3).toFixed(2); message(aud.playbackRate); break; case speedReset: aud.playbackRate = 1; message(''); break; } }, false); function findButtons() { prevButton = $('div.prevbutton') || $('div.prev-icon'); nextButton = $('div.nextbutton') || $('div.next-icon'); playButton = $('div.playbutton') || $('div.playpause') || $('div#big_play_button'); return playButton; } function scrollEmbedPlayer() { $('li.currenttrack')?.scrollIntoView({behavior: 'smooth', block: 'nearest', inline: 'nearest'}); } function $(s) { return document.querySelector(s); } function message(str) { var elm = $('#shortcuts-message'); if (elm) { elm.textContent = str; } } async function changeRelease(direction) { var path = window.location.pathname; if ((path == '/' || path == '/music' || path == '/releases') && direction == 1) { // try clicking 2nd item in the discography on the right document.querySelectorAll('#discography .trackTitle a')[1]?.click(); // try clicking 1st item in the grid list $('li.music-grid-item a , div.ipCellImage a')?.click(); return; } if (!/^\/album|track/.test(path)) { return; } var releasesURL; var cacheMe = false; var labelLink = $('a.back-to-label-link'); if (labelLink && labelLink.textContent.includes('back to')) { releasesURL = new URL(labelLink.href); } else { releasesURL = new URL('https://' + window.location.hostname + '/music'); cacheMe = true; } const lsKey = 'shortcuts-release-data'; if (cacheMe && lsGet(lsKey)) { console.log('fetching release data from cache'); var releaseLinks = lsGet(lsKey); } else { console.log('fetching release data from:', releasesURL.href); var doc = await fetchDOM(releasesURL.href); // get exact href value, a.href after a cross domain fetch adds the wrong domain to /bare/paths var releaseLinks = Array.from( doc.querySelectorAll('li.music-grid-item a , div.ipCellImage a')).map(a => a.getAttribute('href') ); // bandcamp limits the number of releases in the raw HTML, // the rest are available from a data attribute var str = doc.querySelector('#music-grid')?.dataset.clientItems; if (str) { var data = JSON.parse(str); var moreLinks = data.map( ({page_url}) => page_url); releaseLinks = releaseLinks.concat(moreLinks); } // console.log(releaseLinks); if (cacheMe) { lsSet(lsKey, releaseLinks, 1000 * 60 * 10); } } // todo: URL object compare var i = releaseLinks.findIndex(e => e.split('?')[0].endsWith(window.location.pathname)); if (i == -1) { var err = 'error: current item not found on /music page'; console.log(err); message(err); return; } var url = releaseLinks[i + direction]; if (url) { if (!url.startsWith('https://')) { url = 'https://' + releasesURL.hostname + url; } // message(`going to ${(i + 1) + direction} of ${releaseLinks.length}`); console.log('navigating to:', url); window.location = url; } else { var msg = 'no more releases'; if (!cacheMe) { msg += ' (checked label page)'; } message(msg); } } function lsSet(key, value, ttl) { const now = new Date(); const item = { value: value, expiry: now.getTime() + ttl, }; localStorage.setItem(key, JSON.stringify(item)); } function lsGet(key) { const itemStr = localStorage.getItem(key); if (!itemStr) { return null; } const item = JSON.parse(itemStr); const now = new Date(); if (now.getTime() > item.expiry) { localStorage.removeItem(key); return null; } return item.value; } const corsFetch = url => { return new Promise((resolve, reject) => { GM.xmlHttpRequest({ method: "GET", url: encodeURI(url), onload: res => resolve(res.responseText), onerror: res => reject(res) }); }); }; const fetchDOM = async url => { const responseText = await corsFetch(url); const parser = new DOMParser(); return parser.parseFromString(responseText, "text/html"); }; })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址