|
// ==UserScript== |
|
// @name LastFM automated duplicate scrobble deletion script |
|
// @namespace https://gist.github.com/muhdiboy |
|
// @version 1.5.0 |
|
// @downloadURL https://gist.github.com/muhdiboy/a293cbff355af750e3b8f45ec816d1f1/raw/lastfm-automated-remove-duplicates.user.js |
|
// @updateURL https://gist.github.com/muhdiboy/a293cbff355af750e3b8f45ec816d1f1/raw/lastfm-automated-remove-duplicates.user.js |
|
// @description Based on https://gist.github.com/sk22/39cc280840f9d82df574c15d6eda6629#gistcomment-3046698, thanks to previous contributors (@CennoxX, @mattsson, @gms8994, @huw and @sk22) and new contributors (@Eiron). |
|
// @author muhdiboy |
|
// @match https://www.last.fm/*user/* |
|
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== |
|
// @grant none |
|
// ==/UserScript== |
|
|
|
(function() { |
|
// Variables |
|
let runScript = localStorage.getItem('runScript') === 'true'; |
|
let rmOnlyRealDup = localStorage.getItem('rmOnlyRealDup') !== 'false'; // Set checkbox to checked/true as default |
|
let numTracks = localStorage.getItem('numTracks') || "5"; |
|
let stopAtPage = localStorage.getItem('stopAtPage') || "0"; |
|
let currentPage = extractQueryParam('page', window.location.href) || 1; |
|
|
|
// Update currentPage when a click event occurs on the page |
|
window.addEventListener('click', function () { |
|
currentPage = extractQueryParam('page', window.location.href) || 1; |
|
}); |
|
|
|
// Functions |
|
let pageURLCheckInterval = setInterval (function () { |
|
// Check the URL at regular intervals to add or remove buttons |
|
if ( this.lastPathStr !== location.pathname || this.lastQueryStr !== location.search) { |
|
this.lastPathStr = location.pathname; |
|
this.lastQueryStr = location.search; |
|
if (shouldMatchPage()) createButtons(); else removeButtons(); |
|
} |
|
}, 125); |
|
|
|
function replaceQueryParam(param, newval, search) { |
|
// Function to replace a query parameter in the URL |
|
const regex = new RegExp("([?;&])" + param + "[^&;]*[;&]?"); |
|
const query = search.replace(regex, "$1").replace(/&$/, ''); |
|
return (query.length > 2 ? query + "&" : "?") + (newval ? param + "=" + newval : ''); |
|
} |
|
|
|
function extractQueryParam(name, url) { |
|
// Function to extract a query parameter from the URL |
|
if (!url) url = window.location.href; |
|
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); |
|
const regexS = "[\\?&]" + name + "=([^&#]*)"; |
|
const regex = new RegExp(regexS); |
|
const results = regex.exec(url); |
|
return results == null ? null : results[1]; |
|
} |
|
|
|
function shouldMatchPage() { |
|
// Function to determine if the current page should be matched |
|
const url = window.location.href; |
|
const excludeUrlRegex = /^https:\/\/www\.last\.fm\/.*user\/.*\/library\/(albums|artists|tracks|music).*/; |
|
const matchUrlRegex = /^https:\/\/www\.last\.fm\/.*user\/.*\/library.*/; |
|
return matchUrlRegex.test(url) && !excludeUrlRegex.test(url); |
|
} |
|
|
|
function removeButtons() { |
|
// Function to remove all buttons from the page |
|
const cancelButton = document.getElementById('cancelButton'); |
|
const buttonContainer = document.getElementById('buttonContainer'); |
|
|
|
if (cancelButton) cancelButton.remove(); |
|
if (buttonContainer) buttonContainer.remove(); |
|
} |
|
|
|
// Buttons |
|
function createButtons() { |
|
// Create the cancel button to stop the script |
|
const cancelButton = document.createElement('button'); |
|
cancelButton.id = 'cancelButton'; |
|
cancelButton.innerHTML = 'Cancel<br>Script'; |
|
cancelButton.style.position = 'fixed'; |
|
cancelButton.style.bottom = '50%'; |
|
cancelButton.style.left = '0'; |
|
cancelButton.style.zIndex = '9999'; |
|
cancelButton.style.fontFamily = 'monospace'; |
|
cancelButton.style.padding = '3px'; |
|
cancelButton.style.border = 'dashed'; |
|
if (runScript) { |
|
cancelButton.style.color = 'white'; |
|
cancelButton.style.background = 'red'; |
|
cancelButton.style.borderColor = 'firebrick'; |
|
cancelButton.addEventListener('click', function() { |
|
localStorage.setItem('runScript', 'false'); |
|
localStorage.removeItem('savedUrl'); |
|
location.reload(); |
|
}); |
|
} else { |
|
cancelButton.style.color = 'gray'; |
|
cancelButton.style.background = 'lightgray'; |
|
cancelButton.style.borderColor = 'lightgray'; |
|
cancelButton.style.cursor = 'default'; |
|
} |
|
document.body.appendChild(cancelButton); |
|
|
|
if (!runScript) { |
|
// Create the main button to trigger the script |
|
const runButton = document.createElement('button'); |
|
runButton.textContent = 'Remove Duplicates'; |
|
runButton.style.position = 'relative'; |
|
runButton.style.display = 'block'; |
|
runButton.style.marginBottom = '5px'; |
|
runButton.style.fontFamily = 'sans-serif'; |
|
runButton.style.fontSize = 'initial'; |
|
runButton.style.border = 'outset'; |
|
runButton.style.borderColor = 'crimson'; |
|
runButton.style.color = 'white'; |
|
runButton.style.background = 'firebrick'; |
|
runButton.style.padding = '5px'; |
|
runButton.addEventListener('click', function () { |
|
let stopLoop = false; |
|
|
|
while (!stopLoop) { |
|
const inputStopAtPage = prompt("Stop at which page?", stopAtPage); |
|
if (inputStopAtPage === null) return; |
|
if (!/^\d+$/.test(inputStopAtPage)) { |
|
alert("Invalid input. Please enter a valid number."); |
|
continue; |
|
} |
|
if (inputStopAtPage > currentPage && !(currentPage === 0 && inputStopAtPage === 1)) { |
|
alert("Please enter a page less than or equal to your current page: " + currentPage); |
|
continue; |
|
} |
|
localStorage.setItem('stopAtPage', inputStopAtPage); |
|
stopLoop = true; |
|
} |
|
|
|
if (!rmOnlyRealDup) { |
|
stopLoop = false; |
|
while (!stopLoop) { |
|
const inputNumTracks = prompt("Enter the number of tracks to check for duplicates\n(between 2 and 50):", numTracks); |
|
if (inputNumTracks === null) return; |
|
if (!/^\d+$/.test(inputNumTracks) || inputNumTracks < 2 || inputNumTracks > 50) { |
|
alert("Invalid input. Please enter a valid number between 2 and 50."); |
|
continue; |
|
} |
|
localStorage.setItem('numTracks', inputNumTracks); |
|
stopLoop = true; |
|
} |
|
} |
|
|
|
localStorage.setItem('runScript', 'true'); |
|
location.reload(); |
|
}); |
|
|
|
// Create the checkbox element |
|
const rmOnlyRealDupCheckbox = document.createElement('input'); |
|
rmOnlyRealDupCheckbox.type = 'checkbox'; |
|
rmOnlyRealDupCheckbox.id = 'rmOnlyRealDupCheckbox'; |
|
rmOnlyRealDupCheckbox.style.position = 'relative'; |
|
rmOnlyRealDupCheckbox.style.marginLeft = '10px'; |
|
rmOnlyRealDupCheckbox.checked = rmOnlyRealDup; |
|
rmOnlyRealDupCheckbox.addEventListener('change', function () { |
|
localStorage.setItem('rmOnlyRealDup', rmOnlyRealDupCheckbox.checked); |
|
}); |
|
|
|
// Create the label for the checkbox |
|
const rmOnlyRealDupCheckboxLabel = document.createElement('label'); |
|
rmOnlyRealDupCheckboxLabel.style.position = 'relative'; |
|
rmOnlyRealDupCheckboxLabel.style.marginLeft = '5px'; |
|
rmOnlyRealDupCheckboxLabel.style.fontFamily = 'sans-serif'; |
|
rmOnlyRealDupCheckboxLabel.style.fontSize = 'medium'; |
|
rmOnlyRealDupCheckboxLabel.setAttribute('for', 'rmOnlyRealDupCheckbox'); |
|
rmOnlyRealDupCheckboxLabel.textContent = 'Only compare real duplicates?'; |
|
|
|
// Create a container element to hold the main button and checkbox |
|
const buttonContainer = document.createElement('div'); |
|
buttonContainer.id = 'buttonContainer'; |
|
buttonContainer.style.position = 'absolute'; |
|
buttonContainer.style.top = '370px'; |
|
buttonContainer.style.left = '50%'; |
|
buttonContainer.style.transform = 'translate(-50%, -50%)'; |
|
buttonContainer.appendChild(runButton); |
|
buttonContainer.appendChild(rmOnlyRealDupCheckboxLabel); |
|
buttonContainer.appendChild(rmOnlyRealDupCheckbox); |
|
const metaElement = document.querySelector('.content-top-has-nav'); |
|
metaElement.insertAdjacentElement('afterend', buttonContainer); |
|
} |
|
} |
|
|
|
// Automated duplicate scrobble deletion and navigation logic |
|
if (shouldMatchPage()) { |
|
window.addEventListener('load', function () { |
|
// Event listener for when the page has finished loading |
|
if (runScript) { |
|
let found = 0; |
|
const initialRefreshTimeout = 5000; // Interval for reloading the page |
|
let refreshTimeout = parseInt(localStorage.getItem('refreshTimeout')) || initialRefreshTimeout; |
|
const sections = Array.from(document.getElementsByTagName("tbody")); |
|
const savedUrl = localStorage.getItem('savedUrl') || window.location.href; |
|
|
|
// If the URL contains "delete", go back to last functioning site |
|
if (window.location.toString().includes("delete")) window.location.href = savedUrl; |
|
|
|
// If the current page doesn't contain tracks, go back to last functioning site (useful to detect rate-limiting page) |
|
if (!document.querySelector('.chartlist')) { |
|
setTimeout(function() { |
|
window.location.href = savedUrl; |
|
}, refreshTimeout); |
|
localStorage.setItem('refreshTimeout', refreshTimeout + 5000); |
|
return; |
|
|
|
} else { |
|
refreshTimeout = initialRefreshTimeout; |
|
localStorage.setItem('refreshTimeout', initialRefreshTimeout); |
|
localStorage.setItem('savedUrl', window.location.href); |
|
} |
|
|
|
sections.forEach(function (section) { |
|
// Loop through each section |
|
const els = Array.from(section.rows); |
|
const names = els.map(function (el) { |
|
const nmEl = el.querySelector('.chartlist-name'); |
|
const artEl = el.querySelector('.chartlist-artist'); |
|
if (rmOnlyRealDup) { |
|
const tstEl = el.querySelector('.chartlist-timestamp'); |
|
// Construct the name string including track name, artist and timestamp |
|
return nmEl && artEl && tstEl && nmEl.textContent.replace(/\s+/g, ' ').trim() + ':' + artEl.textContent.replace(/\s+/g, ' ').trim() + ':' + tstEl.textContent.replace(/\s+/g, ' ').trim(); |
|
} else { |
|
// Construct the name string including only track name and artist |
|
return nmEl && artEl && nmEl.textContent.replace(/\s+/g, ' ').trim() + ':' + artEl.textContent.replace(/\s+/g, ' ').trim(); |
|
} |
|
}); |
|
|
|
names.forEach(function (name, i) { |
|
// Loop through each name in the section |
|
if (!names.slice(i + 1, i + 1 + parseInt(numTracks)).includes(name)) return; |
|
// Check if the current name has duplicates within the specified range |
|
const delBtn = els[i].querySelector('[data-ajax-form-sets-state="deleted"]'); |
|
// If a delete button is found, click it and increment the counter |
|
if (delBtn) { delBtn.click(); found++; } |
|
}); |
|
}); |
|
|
|
if (found > 0) { |
|
// If duplicates were found, reload the page after refreshTimeout |
|
setTimeout(function() { |
|
location.reload(); |
|
}, refreshTimeout); |
|
return; |
|
} |
|
|
|
if (currentPage <= stopAtPage) { |
|
// Stop the Script and message user |
|
localStorage.setItem('runScript', 'false'); |
|
localStorage.removeItem('savedUrl'); |
|
alert("LastFM duplicate deletion complete."); |
|
location.reload(); |
|
} |
|
|
|
// Go up one page |
|
else window.location.href = window.location.pathname + replaceQueryParam('page', extractQueryParam('page', window.location.href) - 1, window.location.search); |
|
} |
|
}); |
|
} |
|
})(); |
@Jeboose
Can you retest with the newest version? If you have a different issue, please retest on a single page where you are sure multiple duplicates exist and report your findings. I will try to help as possible.
Just to be sure, if you didn't know the feature: the script is differentiating duplicates, ones that are scrobbled at exactly the same time (to the minute) and ones where you can choose the amount to be checked. First one is the true duplicate, enabled by the checkmark. Second one is active when the checkmark is unchecked. When unchecked, another input box will appear after hitting the "Remove duplicates" button. There you can input the number of scrobbles to compare to.
This is described in the doc above: Using the script - step by step