|
// ==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); |
|
} |
|
}); |
|
} |
|
})(); |
Hello @Eiron ,
thank you very much for your input! It makes the script much more stable.
I released a new version with your additions and also added a variable to enable or disable removing only true duplicates. The reason for my addition is because I have a bug with my wireless earbuds that seem to play the last track on loop while they are not being used. The option is there for people who want to change the (now) default behaviour.
@alxjms92, also thank you for your feedback. I'm still not sure why that happens, as I've tested the script on fresh Firefox and Chrome installations. Maybe this new addition will be helpful. Test it out and report your experience please. Thank you!