Last.fm Bulk Delete Scrobbles

This script allows a bulk delete of Last.fm scrobbles

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Last.fm Bulk Delete Scrobbles
// @description  This script allows a bulk delete of Last.fm scrobbles
// @namespace    http://tampermonkey.net/
// @version      1.1
// @author       xXMrJackXx (updated by finnnjake)
// @match        https://*.last.fm/user/*
// @exclude      https://*.last.fm/user/*/loved
// @require      http://code.jquery.com/jquery-3.3.1.min.js
// @grant        GM_addStyle
// @grant        unsafeWindow
// @run-at       document-end
// @license MIT
// ==/UserScript==

// please note this is an edited version of xxMrJackXx's original script (greasyfork.org/en/scripts/370386-last-fm-bulk-delete-scrobbles), which I have updated and is working as of 22 Dec 25 on thorium 130.0.6723.174 . Has large clickable areas and the ability to shift click. Also for clarity this was done with AI

/* globals $, exportFunction */

(function() {
    'use strict';

    // Track last clicked checkbox for shift+click range selection
    let lastClickedIndex = null;

    GM_addStyle(`
    .batch-button {
      width: 100px;
      margin-right: 10px;
      border-radius: 6px;
      outline: none;
    }
    .batch-checkbox-cell {
      width: 50px;
      cursor: pointer;
      text-align: center;
      vertical-align: middle;
    }
    .batch-checkbox-cell:hover {
      background-color: rgba(185, 0, 0, 0.1);
    }
    .batch-checkbox {
      cursor: pointer;
      width: 18px;
      height: 18px;
      pointer-events: none;
    }
    `);

    const observer = new MutationObserver(() => $('.chartlist') && process());

    // clear localStorage on first load
    localStorage.clear();
    process();
    registerPJax();

    function init() {
        const main = $(".col-main");
        const div = $("<div id='batchDeleteButtons'>");
        main.prepend(div);

        // add delete button
        const deleteButton = $('<input type="button" value="Delete" class="batch-button"/>');
        deleteButton.click(function() {
            $('table.chartlist').find('input:checked').each(function() {
                // find delete button and simulate a click of every checked checkbox
                const del = this.closest('tr').querySelector('[data-ajax-form-sets-state="deleted"]');
                if (del) { del.click(); }
                // remove from localStorage
                localStorage.removeItem(this.id);
                // remove checkbox
                this.remove();
            });
        });
        div.prepend(deleteButton);

        // add check/uncheck all button
        const checkButton = $('<input type="button" value="Check all" data-type="check" class="batch-button"/>');
        checkButton.click(function() {
            const cbs = $(main).find('input.batch-checkbox');
            if ($(this).attr('data-type') === 'check') {
                $(this).attr('data-type', 'uncheck');
                $(this).val('Uncheck all');
                cbs.prop('checked', true);
                // add every checkbox id to the localStorage
                cbs.each(function() {
                    localStorage.setItem(this.id, this.id);
                });
            } else {
                $(this).attr('data-type', 'check');
                $(this).val('Check all');
                cbs.prop('checked', false);
                // remove every checkbox id from the localStorage
                cbs.each(function() {
                    localStorage.removeItem(this.id);
                });
            }
        });
        div.prepend(checkButton);
    }

    function handleCheckboxChange(checkbox, checked) {
        checkbox.checked = checked;
        if (checked) {
            localStorage.setItem(checkbox.id, checkbox.id);
        } else {
            localStorage.removeItem(checkbox.id);
        }
    }

    function handleCellClick(e) {
        const cell = e.currentTarget;
        const checkbox = cell.querySelector('input.batch-checkbox');
        if (!checkbox) return;

        const allCheckboxes = Array.from(document.querySelectorAll('input.batch-checkbox'));
        const currentIndex = allCheckboxes.indexOf(checkbox);
        const newCheckedState = !checkbox.checked;

        // Shift+click: select range
        if (e.shiftKey && lastClickedIndex !== null && lastClickedIndex !== currentIndex) {
            const start = Math.min(lastClickedIndex, currentIndex);
            const end = Math.max(lastClickedIndex, currentIndex);

            for (let i = start; i <= end; i++) {
                handleCheckboxChange(allCheckboxes[i], newCheckedState);
            }
        } else {
            // Normal click: toggle single checkbox
            handleCheckboxChange(checkbox, newCheckedState);
        }

        lastClickedIndex = currentIndex;
    }

    function process() {
        observer.disconnect();

        // add checkboxes for every not already deleted entries
        $('tr[data-ajax-form-state!="deleted"] .chartlist-play').each(function() {
            const track = $(this).siblings('.chartlist-name').find('a[title]').attr('title');
            const timestamp = $(this).siblings('.chartlist-timestamp').find('span[title]').attr('title');
            if (track !== undefined && timestamp !== undefined) {
                // combination of timestamp and track title should be unique
                const id = normalizeString(timestamp + track);
                const cb = $('<input type="checkbox" class="batch-checkbox" id="' + id + '"/>');
                // Create cell with larger clickable area
                const td = $('<td class="batch-checkbox-cell"/>');
                td.append(cb);
                td.on('click', handleCellClick);
                td.insertBefore($(this));
            }
        });

        // reload localStorage items
        const keys = Object.keys(localStorage);
        let i = keys.length;
        while (i--) {
            // check every checkbox that is contained in the localStorage
            const id = keys[i];
            const cb = $('#' + id);
            if (cb.length) {
                cb.prop('checked', true);
            }
        }

        if ($('.chartlist tbody').length) {
            observer.takeRecords();
            observer.observe($('.chartlist tbody')[0], {childList: true});

            // add buttons only if not already present
            if ($('#batchDeleteButtons').length === 0) {
                init();
            }
        }
    }

    function registerPJax() {
        document.addEventListener('pjax:end:batch-delete', process);
        window.addEventListener('load', function onLoad() {
            window.removeEventListener('load', onLoad);
            unsafeWindow.jQuery(unsafeWindow.document).on('pjax:end', exportFunction(() => document.dispatchEvent(new CustomEvent('pjax:end:batch-delete')), unsafeWindow));
        });
    }

    function normalizeString(str) {
        return str.replace(/[^a-z0-9\s]/gi, '').replace(/[_\s]/g, '-');
    }
})();