|
// ==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 used it again today and noticed a warning from Tampermonkey about Chromium moving to Manifest V3 next year. So I changed the include parameter to match. Also some small changes to the comments and arrangement in the script.
Also I noticed that the script didn't run well and stopped after a few reloads when the tab ran in background. This didn't happen before and might be because of other running websites (Youtube, Twitch). Moving the tab to a seperate window solved the issue. So just a suggestion if people have the same difficulty to move the script running tab to a seperate window.