Finds and highlights "Name Your Price" (NYP) albums on Bandcamp discover pages..
// ==UserScript==
// @name Bandcamp Name Your Price Finder
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Finds and highlights "Name Your Price" (NYP) albums on Bandcamp discover pages..
// @author f0
// @match https://bandcamp.com/discover
// @match https://bandcamp.com/discover/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @connect bandcamp.com
// @connect *.bandcamp.com
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// --- 1. Constants and State ---
const ALBUM_SELECTOR = 'li.results-grid-item:not([data-nyp-checked])';
const state = {
isRunning: false,
nypFound: 0,
};
// --- 2. UI Setup ---
function setupUI() {
GM_addStyle(`
#nyp-finder-container {
position: fixed; bottom: 20px; right: 20px; z-index: 9999;
background-color: #1a1a1a; color: #fff; padding: 15px;
border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.3);
font-family: sans-serif; min-width: 240px;
display: flex; flex-direction: column; gap: 10px;
}
.nyp-finder-button {
background-color: #629aa9; color: white; border: none;
padding: 10px 15px; border-radius: 5px; cursor: pointer; font-size: 14px;
}
.nyp-finder-button:hover:not(:disabled) { filter: brightness(1.1); }
.nyp-finder-button:disabled { background-color: #555; cursor: not-allowed; }
#nyp-fetch-all-button { background-color: #4CAF50; }
#nyp-fetch-limited-button { background-color: #f0ad4e; }
#nyp-finder-status { font-size: 12px; color: #ccc; min-height: 1.2em; text-align: center; }
.nyp-album-highlight { border: 3px solid #ff69b4 !important; box-shadow: 0 0 10px #ff69b4; border-radius: 4px; }
.nyp-album-label {
position: absolute; top: 5px; left: 5px; z-index: 10;
background-color: #ff69b4; color: white; padding: 2px 5px;
font-size: 12px; font-weight: bold; border-radius: 3px;
}
.nyp-controls-row { display: flex; gap: 8px; align-items: center; }
#nyp-fetch-count-input {
width: 60px; background: #333; border: 1px solid #555;
color: white; border-radius: 4px; padding: 8px; text-align: center;
}
.nyp-filter-container { display: flex; align-items: center; justify-content: flex-end; font-size: 13px; gap: 6px; cursor: pointer; color: #eee; }
`);
const container = document.createElement('div');
container.id = 'nyp-finder-container';
container.innerHTML = `
<div class="nyp-controls-row">
<button id="nyp-fetch-limited-button" class="nyp-finder-button" style="flex-grow: 1;">Fetch X Albums</button>
<input type="number" id="nyp-fetch-count-input" placeholder="200" min="1">
</div>
<button id="nyp-fetch-all-button" class="nyp-finder-button">Fetch All Albums</button>
<button id="nyp-finder-button" class="nyp-finder-button">Find "Name Your Price"</button>
<div id="nyp-finder-status">Ready to search.</div>
<label class="nyp-filter-container">
<input type="checkbox" id="nyp-filter-checkbox"> Show NYP Only
</label>
`;
document.body.appendChild(container);
return {
findButton: container.querySelector('#nyp-finder-button'),
fetchAllButton: container.querySelector('#nyp-fetch-all-button'),
fetchLimitedButton: container.querySelector('#nyp-fetch-limited-button'),
fetchCountInput: container.querySelector('#nyp-fetch-count-input'),
statusDiv: container.querySelector('#nyp-finder-status'),
filterCheckbox: container.querySelector('#nyp-filter-checkbox'),
buttons: container.querySelectorAll('.nyp-finder-button'),
};
}
// --- 3. Core Logic ---
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
function setUiState(loading, message) {
state.isRunning = loading;
ui.buttons.forEach(btn => btn.disabled = loading);
ui.statusDiv.textContent = message;
}
/**
* FIXED: This function now properly parses the HTML to accurately find NYP albums.
*/
function isNameYourPrice(albumElement) {
return new Promise((resolve) => {
const link = albumElement.querySelector('a[href*=".bandcamp.com/album/"]');
if (!link) return resolve(false);
GM_xmlhttpRequest({
method: "GET",
url: link.href,
onload: (response) => {
try {
const doc = document.createElement('html');
doc.innerHTML = response.responseText;
// 1. Find the specific "Buy Digital Album" section
const digitalBuyItem = doc.querySelector('.buyItem.digital');
if (!digitalBuyItem) return resolve(false);
// 2. Look for the "Name Your Price" indicator *within* that section
const nypSpan = digitalBuyItem.querySelector('.buyItemExtra.buyItemNyp');
const isNyp = nypSpan && nypSpan.textContent.trim().toLowerCase().includes('name your price');
resolve(!!isNyp);
} catch (e) {
console.error(`Error parsing ${link.href}:`, e);
resolve(false);
}
},
onerror: (error) => {
console.error(`Error fetching ${link.href}:`, error);
resolve(false);
}
});
});
}
function highlightAlbum(albumElement) {
albumElement.classList.add('nyp-album-highlight');
const imageContainer = albumElement.querySelector('section.image-carousel, .art');
if (imageContainer) {
imageContainer.style.position = 'relative';
const label = document.createElement('div');
label.className = 'nyp-album-label';
label.textContent = 'NYP';
imageContainer.appendChild(label);
}
}
async function fetchAlbums(limit = Infinity) {
if (state.isRunning) return;
setUiState(true, 'Fetching albums...');
try {
const albumsPerPage = 48;
let clicksNeeded = Infinity;
if (limit !== Infinity) {
const currentCount = document.querySelectorAll('li.results-grid-item').length;
const albumsToLoad = limit - currentCount;
if (albumsToLoad <= 0) {
setUiState(false, `Already have ${currentCount} albums.`);
return;
}
clicksNeeded = Math.ceil(albumsToLoad / albumsPerPage);
}
for (let i = 0; i < clicksNeeded; i++) {
const viewMoreButton = document.getElementById('view-more');
if (!viewMoreButton || viewMoreButton.offsetParent === null) break;
const totalAlbums = document.querySelectorAll('li.results-grid-item').length;
setUiState(true, `Loading... (${totalAlbums} loaded)`);
viewMoreButton.click();
await sleep(1500);
}
} finally {
const finalCount = document.querySelectorAll('li.results-grid-item').length;
setUiState(false, `Loaded ${finalCount} albums. Ready to find NYP.`);
}
}
async function processAlbums() {
if (state.isRunning) return;
const albumElements = document.querySelectorAll(ALBUM_SELECTOR);
if (albumElements.length === 0) {
setUiState(false, `All albums checked. Total found: ${state.nypFound}`);
return;
}
setUiState(true, 'Starting scan...');
const showNypOnly = ui.filterCheckbox.checked;
try {
let newFound = 0;
for (const [index, el] of albumElements.entries()) {
setUiState(true, `Checking ${index + 1}/${albumElements.length}... (Found: ${state.nypFound})`);
el.dataset.nypChecked = 'true';
if (await isNameYourPrice(el)) {
state.nypFound++;
newFound++;
highlightAlbum(el);
} else if (showNypOnly) {
el.style.display = 'none';
}
await sleep(100);
}
setUiState(false, `Scan complete. Found ${newFound} more. Total: ${state.nypFound}.`);
} catch (error) {
console.error("An error occurred during album processing:", error);
setUiState(false, "An error occurred. Check console.");
}
}
function initObserver() {
const targetNode = document.querySelector('.results-grid ul.items');
if (!targetNode) return setTimeout(initObserver, 500);
const observer = new MutationObserver((mutationsList) => {
if (state.isRunning) return;
for (const mutation of mutationsList) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
setUiState(false, 'New albums loaded. Click "Find" to check them.');
break;
}
}
});
observer.observe(targetNode, { childList: true });
}
// --- 4. Initialization ---
const ui = setupUI();
ui.findButton.addEventListener('click', processAlbums);
ui.fetchAllButton.addEventListener('click', () => fetchAlbums(Infinity));
ui.fetchLimitedButton.addEventListener('click', () => {
const count = parseInt(ui.fetchCountInput.value, 10);
if (isNaN(count) || count <= 0) {
setUiState(false, 'Please enter a valid number.');
return;
}
fetchAlbums(count);
});
initObserver();
})();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址