Bandcamp Tools

Adds QoL improvements to Bandcamp.

当前为 2024-09-09 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Bandcamp Tools
// @namespace    http://violentmonkey.net/
// @version      1.0
// @description  Adds QoL improvements to Bandcamp.
// @author       InariOkami
// @match        *://*.bandcamp.com/*
// @grant        none
// @icon          https://cdn-icons-png.flaticon.com/512/21/21496.png
// ==/UserScript==

(function() {
    'use strict';

    var app = {
        id: "bcp-sp"
    };

    app.debug = false;

    var cls = {
        price: app.id + '-price',
        handled: app.id + '-handled'
    };

    var selectors = {
        product: 'li[data-trackid]:not(.' + cls.handled + ')'
    };

    function findOne(selector, context, dontYell) {
        context = context || document;
        var item = context.querySelector(selector);
        if (item && app.debug) {
            console.log(app.id, ': found element matching "' + selector + '"');
        } else if (!item && !dontYell) {
            console.warn(app.id, ': found no element for selector "' + selector + '"');
        }
        return item;
    }

    function findFirst(selector, context) {
        return findAll(selector, context)[0];
    }

    function findAll(selector, context, dontYell) {
        if (!selector || !selector.length || selector.length === 1) {
            console.error(app.id, ': incorrect selector : ', selector);
        }
        context = context || document;
        var items = Array.prototype.slice.call(context.querySelectorAll(selector));
        if (items.length && app.debug) {
            console.log(app.id, ': found', items.length, 'elements matching "' + selector + '"');
        } else if (!items.length && !dontYell) {
            console.warn(app.id, ': found no elements for selector "' + selector + '"');
        }
        return items;
    }

    function debounce(func, wait, immediate) {
        var timeout;
        return function () {
            var context = this;
            var args = arguments;
            var later = function later() {
                timeout = null;
                if (!immediate) {
                    func.apply(context, args);
                }
            };
            var callNow = immediate && !timeout;
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
            if (callNow) {
                func.apply(context, args);
            }
        };
    }

    function cleanPrevious() {
        findAll('[class^="' + cls.price + '"]', document, true).forEach(function (node) {
            return node.remove();
        });
    }

    function displayPrice(product, price) {
        var tag = document.createElement('div');
        tag.innerHTML = price.value + ' <small>' + price.currency + '</small>';
        tag.style = 'position: absolute; top: 0; right: 0; background-color: green; color: white;';
        tag.classList.add(cls.price, 'col-edit-box');
        product.appendChild(tag);
        if (price.value > 2) {
            product.style.filter = 'grayscale(1) opacity(.5)';
        }
        product.classList.add(cls.handled);
    }

    function displayPrices() {
        findAll(selectors.product, document, true).forEach(function (product) {
            var trackid = parseInt(product.getAttribute('data-trackid'));
            if (trackid) {
                if (app.debug) {
                    console.log(app.id, ': adding price for', trackid);
                }
                if (!app.tracks.hasOwnProperty(trackid)) {
                    throw new Error('failed at gettting track price');
                }
                var price = app.tracks[trackid];
                displayPrice(product, price);
            }
        });
    }

    function setTracksFromList(list) {
        if (!app.tracks) {
            app.tracks = {};
        }
        var added = 0;
        list.map(function (track) {
            var trackid = track.track_id;
            if (!app.tracks.hasOwnProperty(trackid)) {
                app.tracks[trackid] = {
                    value: Math.round(track.price),
                    currency: track.currency
                };
                added++;
            }
        });
        console.log(app.id, ': added', added, 'tracks to local db :D');
    }

    function getDataFromApi() {
        fetch('https://bandcamp.com/api/fancollection/1/wishlist_items', {
            method: 'POST',
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                fan_id: app.userid,
                older_than_token: app.token
            })
        }).then(function (json) {
            return json.json();
        }).then(function (data) {
            app.token = data.last_token;
            setTracksFromList(data.track_list);
            if (data.more_available) {
                getDataFromApi();
            }
        });
    }

    function getDataFromPage() {
        var dataEl = findOne('#pagedata');
        var data = JSON.parse(dataEl.getAttribute('data-blob'));
        setTracksFromList(data.track_list);
        app.token = data.wishlist_data.last_token;
        app.userid = data.fan_data.fan_id;
    }

    function process() {
        displayPrices();
    }

    function init() {
        console.log(app.id, ': init !');
        cleanPrevious();
        getDataFromPage();
        getDataFromApi();
        process();
    }

    init();

    var processDebounced = debounce(process, 500);
    document.addEventListener('scroll', processDebounced);

})();

(function() {
    'use strict';
    var table = $('#track_table tbody tr')
    , tdata = table ? table.each(function(){}) : []
    , adata = window.TralbumData || false;
    if (table.length > 1 && adata) {
        for(var i = 0; adata.trackinfo[i]; i++) {
            var p = $($('tr td .dl_link')[i]), link = document.createElement("a"), track = adata.trackinfo[i];
            link.href = track.file["mp3-128"];
            link.title = link.download = track.track_num + " - " + track.title + ".mp3";
            link.alt = 'If left clicking opens song, right click to download.';
            link.innerHTML = 'Download!';
            p.html(link);
        }
    } else {
        $('.inline_player').append('<br /><strong><a href="#" class="font-size: 18px;" onclick="location.href=TralbumData.trackinfo[0].file["mp3-128"]">Download Now! (128kb MP3)</a></strong><br />');
    }
    alert("jestem");

})();

(function() {
    'use strict';
    const DBG = false;

    let log = function(s) {
            return (DBG && console.log(s));
        },
        qS = function(el, scope) {
            scope = (typeof scope === 'object') ? scope : document;
            return scope.querySelector(el) || false;
        },
        qSall = function(els, scope) {
            scope = (typeof scope === 'object') ? scope : document;
            return scope.querySelectorAll(els) || false;
        },
        hidden, visibilityChange, state,
        tabFocused = function(evt) {
            log("tab has focus!");
            if (document !== evt.target) {
                log("warning, document not equal to document. it is ");
                log(evt.target); // should be document
            }
            if (evt.target.body !== document.activeElement) {
                log("warning, document.activeElement not equal to document.body. it is ");
                log(evt.target.body); // should be document.body
            }
        },
        elmTarget = qS("#trackInfoInner > div.inline_player > table > tbody > tr:nth-child(1) > td.play_cell > a > div");
    if (!elmTarget) {
        log('main play button not found, exiting');
        return;
    }

    if (typeof document.hidden !== "undefined") { // Opera 12.10 and Firefox 18 and later support
        hidden = "hidden";
        visibilityChange = "visibilitychange";
        state = "visibilityState";
    } else if (typeof document.mozHidden !== "undefined") {
        hidden = "mozHidden";
        visibilityChange = "mozvisibilitychange";
        state = "mozVisibilityState";
    } else if (typeof document.msHidden !== "undefined") {
        hidden = "msHidden";
        visibilityChange = "msvisibilitychange";
        state = "msVisibilityState";
    } else if (typeof document.webkitHidden !== "undefined") {
        hidden = "webkitHidden";
        visibilityChange = "webkitvisibilitychange";
        state = "webkitVisibilityState";
    }

    if ('undefined' === typeof hidden) {
        log('document.hidden not found, exiting');
        return;
    }

    document.addEventListener(visibilityChange, function(e) {
        return (false === document[hidden]) && tabFocused(e);
    });
    window.addEventListener('keydown', function(e) {
        log("in keydown");
        if(e.key === " " && e.target === document.body) {
            log("keydown ok");
            e.preventDefault();
        }
    });
    qS('body').addEventListener("keyup", function(e) {
        log("in keyup");
        if (e.key === " " && e.target === document.body) {
            e.preventDefault();
            elmTarget.focus();
            elmTarget.click();
            elmTarget.blur();
            log("keyup ok");
        }
    });
})();

var gen = document.querySelector("meta[name=generator]");
if(!gen || gen.content != "Bandcamp") {
    return;
}

var style = document.createElement("style");
style.textContent = ".volumeControl{align-items:center;display:flex;height:52px;margin-top:1em}.volumeControl .thumb{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.volumeControl>.speaker{background:#fff;border:1px solid #d9d9d9;border-radius:2px;color:#000;cursor:pointer;font-size:32px;height:54px;line-height:54px;position:relative;text-align:center;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;width:54px}.volumeControl>.speaker>svg{margin:11px}";
document.head.appendChild(style);

var dragWidth = 226;
var dragging = false;
var dragPos = 0;
var percentage = parseFloat(localStorage.getItem("volume")) || 0.5;
var speaker, volumeInner, audio, volume;

function onLoad() {
    audio = document.getElementsByTagName("audio")[0];
    updateVolume();

    var container = document.createElement("div");
    container.classList.add("volumeControl");

    speaker = document.createElement("i");
    speaker.classList.add("speaker");
    speaker.addEventListener("click", function () {
        audio.muted = !audio.muted;
        updateHtml();
    });
    container.appendChild(speaker);

    var volume = document.createElement("div");
    volume.classList.add("progbar");
    container.appendChild(volume);

    var fill = document.createElement("div");
    fill.classList.add("progbar_empty");
    fill.style.width = "248px";
    volume.appendChild(fill);

    volumeInner = document.createElement("div");
    volumeInner.classList.add("thumb");
    
    volumeInner.addEventListener("mousedown", function (e) {
        dragging = true;
        dragPos = e.offsetX;
    });
    fill.appendChild(volumeInner);
    
    document.querySelector(".inline_player").appendChild(container);

    updateHtml();

    document.addEventListener("mouseup", function () {
        if (dragging) {
            localStorage.setItem("volume", percentage);
            dragging = false;
        }
    });
    document.addEventListener("mousemove", function (e) {
        if (dragging) {
            var pos = volume.getBoundingClientRect();

            audio.muted = false;
            percentage = clamp(((e.pageX - pos.left) - dragPos) / dragWidth, 0, 1);
            updateVolume();
            updateHtml();
        }
    });
}

if (document.readyState == 'complete') {
    onLoad();
} else {
    window.addEventListener("load", onLoad);
}

function updateVolume() {
    audio.volume = (Math.exp(percentage) - 1) / (Math.E - 1);
}

function updateHtml() {
    // svgs from https://www.material.io/resources/icons
    if (audio.muted) {
        speaker.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" height="32" viewBox="0 0 24 24" width="32"><path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/><path d="M0 0h24v24H0z" fill="none"/></svg>';
        volumeInner.style.left = "0%";
    } else {
        if (percentage <= 0) {
            speaker.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" height="32" viewBox="0 0 24 24" width="32"><path d="M7 9v6h4l5 5V4l-5 5H7z"/><path d="M0 0h24v24H0z" fill="none"/></svg>';
        } else if (percentage < 0.5) {
            speaker.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" height="32" viewBox="0 0 24 24" width="32"><path d="M18.5 12c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM5 9v6h4l5 5V4L9 9H5z"/><path d="M0 0h24v24H0z" fill="none"/></svg>';
        } else {
            speaker.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" height="32" viewBox="0 0 24 24" width="32"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/><path d="M0 0h24v24H0z" fill="none"/></svg>';
        }
        volumeInner.style.left = dragWidth * percentage + 'px';
    }
}

function clamp(num, min, max) {
    return num <= min ? min : num >= max ? max : num;
}