|
// ==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); |
|
} |
|
}); |
|
} |
|
})(); |
I'm also maintaining a Last.fm script and have dealt with this issue myself.
I think the best course of action is to make the script detect the rate-limiting page, and automatically pauses for 10/20/30/etc. seconds before automatically trying again.
I advice against detecting the text on the page. The text might differ based on the user language. Instead, I would simply test if the page contains a <table> (or if you want to be really specific, a table with the
chartlist
CSS class).A Last.fm script I wrote stores the amount of seconds to wait in a variable. The variable starts at 10 seconds. So, my Last.fm script only waits 10 seconds the first time it gets rate-limited. If it can successfully resume (without immediately getting rate-limited again) after 10 seconds, my script will continue loading pages. The next time it gets rate limited, it will still only wait 10 seconds. However, as soon as it detects rate-limiting immediately after waiting 10 seconds, it increases the variable by 10 seconds. From that moment onwards, the script will wait 20 seconds every time it gets rate-limited. When the script detects rate-limiting immediately after waiting 20 seconds, the variable is increased to 30 seconds. It goes on like that until the variable reaches 60 seconds. This approach might sound convoluted but it allows my script to load pages relatively fast without knowing the exact details of Last.fm's undocumented rate-limiting.