Last active
November 12, 2025 17:37
-
-
Save strictlymomo/1a0632db222991e30b53732feb7c884f to your computer and use it in GitHub Desktop.
Bandcamp Digging Tool
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
| /** | |
| * BANDCAMP DIGGING TOOL | |
| * Browse any user's collection with ease | |
| * | |
| * How To Use This: | |
| * 1. Open a URL (e.g. https://bandcamp.com/noemsel) | |
| * 2. Run this script in the dev tools console | |
| * 3. Hover over a record to hear it instantaneously | |
| * 4. Keys | |
| * - `z` rewind | |
| * - k` fast-forward | |
| * - `c` equivalent to clicking "Buy Now" button | |
| */ | |
| let nowPlaying = null; | |
| let hoverTimeout = null; | |
| const HOVER_DELAY = 100; | |
| /** | |
| * Injects CSS to make the collection grid full-width for a more immersive digging experience | |
| */ | |
| const transcendGrid = () => { | |
| const style = document.createElement("style"); | |
| style.textContent = ` | |
| .fan-container .grid { width: 100% !important; } | |
| @media (min-width: 1232px) { | |
| ol.collection-grid { width: 100% !important; } | |
| } | |
| `; | |
| document.head.appendChild(style); | |
| }; | |
| /** | |
| * Sets up enhanced functionality to dig through collection items | |
| */ | |
| const initDigger = () => { | |
| const items = document.querySelectorAll(".collection-item-container"); | |
| if (items.length === 0) return; | |
| /** | |
| * Handles keyboard shortcuts for seeking (left/right arrows skip 30s) | |
| */ | |
| const handleKeydown = (e) => { | |
| const audio = document.querySelector("audio"); | |
| if (!audio || !nowPlaying) return; | |
| const skipAmount = 30; | |
| if (e.key === "ArrowLeft") { | |
| e.preventDefault(); | |
| audio.currentTime = Math.max(0, audio.currentTime - skipAmount); | |
| console.log( | |
| `⏪ Skipped back ${skipAmount}s to ${audio.currentTime.toFixed(1)}s` | |
| ); | |
| } else if (e.key === "ArrowRight") { | |
| e.preventDefault(); | |
| audio.currentTime = Math.min( | |
| audio.duration, | |
| audio.currentTime + skipAmount | |
| ); | |
| console.log( | |
| `⏩ Skipped forward ${skipAmount}s to ${audio.currentTime.toFixed(1)}s` | |
| ); | |
| } | |
| }; | |
| /** | |
| * Stops playback of the previously playing item if it's different from current | |
| */ | |
| const stopPreviousItem = (item) => { | |
| if (nowPlaying && nowPlaying !== item) { | |
| const prevPlayLink = nowPlaying.querySelector(".track_play_auxiliary"); | |
| if (prevPlayLink && nowPlaying.classList.contains("playing")) { | |
| prevPlayLink.click(); | |
| } | |
| } | |
| }; | |
| /** | |
| * Seeks the audio element to the middle of the track | |
| * Returns true if successful, false if audio not ready | |
| */ | |
| const seekToMiddle = () => { | |
| const audio = document.querySelector("audio"); | |
| if (audio && audio.readyState >= 2) { | |
| const seekTime = audio.duration / 2; | |
| if (seekTime && !isNaN(seekTime)) { | |
| audio.currentTime = seekTime; | |
| } | |
| return true; | |
| } | |
| return false; | |
| }; | |
| /** | |
| * Starts playback of an item and seeks to the middle | |
| */ | |
| const startPlayback = (item) => { | |
| stopPreviousItem(item); | |
| const playLink = item.querySelector(".track_play_auxiliary"); | |
| if (!playLink) return; | |
| if (!item.classList.contains("playing")) { | |
| playLink.click(); | |
| } | |
| nowPlaying = item; | |
| const checkAudio = setInterval(() => { | |
| if (seekToMiddle()) { | |
| clearInterval(checkAudio); | |
| } | |
| }, 100); | |
| setTimeout(() => clearInterval(checkAudio), 3000); | |
| }; | |
| /** | |
| * Initiates playback after a hover delay | |
| */ | |
| const handleMouseEnter = (item) => { | |
| hoverTimeout = setTimeout(() => startPlayback(item), HOVER_DELAY); | |
| }; | |
| /** | |
| * Stops playback when mouse leaves and clears any pending hover timeout | |
| */ | |
| const handleMouseLeave = (item) => { | |
| if (hoverTimeout) { | |
| clearTimeout(hoverTimeout); | |
| hoverTimeout = null; | |
| } | |
| if (nowPlaying === item) { | |
| const playLink = item.querySelector(".track_play_auxiliary"); | |
| if (playLink && item.classList.contains("playing")) { | |
| playLink.click(); | |
| } | |
| nowPlaying = null; | |
| } | |
| }; | |
| document.addEventListener("keydown", handleKeydown); | |
| items.forEach((item) => { | |
| const artContainer = item.querySelector(".collection-item-art-container"); | |
| if (!artContainer) return; | |
| artContainer.addEventListener("mouseenter", () => handleMouseEnter(item)); | |
| artContainer.addEventListener("mouseleave", () => handleMouseLeave(item)); | |
| }); | |
| console.log(`✓ Hover preview enabled on ${items.length} items`); | |
| }; | |
| /** | |
| * Automatically scrolls and clicks "show more" to load all collection items | |
| * Stops after reaching maxItems or when no new items load after 3 attempts | |
| */ | |
| const loadMoreItems = async (maxItems = 500) => { | |
| let previousCount = 0; | |
| let noChangeCount = 0; | |
| console.log("Loading more items..."); | |
| while (true) { | |
| const currentCount = document.querySelectorAll( | |
| ".collection-item-container" | |
| ).length; | |
| if (currentCount >= maxItems) { | |
| console.log(`Reached ${currentCount} items (max: ${maxItems})`); | |
| break; | |
| } | |
| if (currentCount === previousCount) { | |
| noChangeCount++; | |
| if (noChangeCount >= 3) { | |
| console.log(`No more items to load (${currentCount} total)`); | |
| break; | |
| } | |
| } else { | |
| noChangeCount = 0; | |
| } | |
| previousCount = currentCount; | |
| console.log(`Loaded ${currentCount} items so far...`); | |
| window.scrollTo(0, document.body.scrollHeight); | |
| await new Promise((resolve) => setTimeout(resolve, 500)); | |
| const showMoreButton = document.querySelector( | |
| ".expand-container .show-more" | |
| ); | |
| if (showMoreButton && showMoreButton.offsetParent !== null) { | |
| console.log("Clicking show more button..."); | |
| showMoreButton.click(); | |
| await new Promise((resolve) => setTimeout(resolve, 1000)); | |
| } | |
| await new Promise((resolve) => setTimeout(resolve, 1500)); | |
| } | |
| window.scrollTo(0, 0); | |
| }; | |
| loadMoreItems(); | |
| transcendGrid(); | |
| initDigger(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment