您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds an "Edit scrobble" entry to the context menu of Last.fm
// ==UserScript== // @name Edit Last.fm Scrobbles // @version 0.4.6 // @description Adds an "Edit scrobble" entry to the context menu of Last.fm // @author CennoxX, nicoleahmed // @namespace https://greasyfork.org/users/21515 // @homepage https://github.com/CennoxX/userscripts // @supportURL https://github.com/CennoxX/userscripts/issues/new?title=[Edit%20Last.fm%20Scrobbles]%20 // @match https://www.last.fm/*user* // @match https://www.last.fm/api?* // @connect ws.audioscrobbler.com // @icon https://www.google.com/s2/favicons?sz=64&domain=last.fm // @grant GM.xmlHttpRequest // @license MIT // ==/UserScript== /* jshint esversion: 11 */ /* eslint curly: "off" */ (function main() { "use strict"; var api_key = "7bfc3993e87eb839bd1567bd2622dd56"; var username = localStorage.getItem("username"); var sessionKey = localStorage.getItem("sessionKey"); authenticate(); reloadOnPageChange(); addEditButtonToMenu(); function authenticate(){ if (!sessionKey){ var token = location.href.split("?token=")[1]; if (token){ document.querySelector(".error-page-marvin").style = "display: none"; setSuccessPage("Connecting …", "", ""); var data = "api_key=" + api_key + "&token=" + token + "&method=auth.getsession"; GM.xmlHttpRequest({ method: "GET", url: "https://ws.audioscrobbler.com/2.0/?" + data + "&api_sig=" + lfmmd5(data) + "&format=json", onload: function(response) { if (response.responseText.length > 0) { var jsonObj = JSON.parse(response.responseText); username = jsonObj.session.name; localStorage.setItem("username", username); sessionKey = jsonObj.session.key; localStorage.setItem("sessionKey", sessionKey); setSuccessPage("Connected", "Access allowed!", "The Edit scrobble feature of Edit Last.fm Scrobbles is now enabled."); } }, onerror: function(response) { console.log("Error in fetching contents: " + response.responseText); } }); } else { window.location.replace("https://www.last.fm/api/auth?api_key=" + api_key + "&cb=https://www.last.fm/api"); } } } function setSuccessPage(title, intro, description){ document.title = title + " | Last.fm"; document.querySelector("h1").innerHTML = title; document.querySelector(".page-content p").innerHTML = intro; document.querySelector(".page-content p~p").innerHTML = description; } function reloadOnPageChange(){ var oldChartlist = document.querySelector(".chartlist"); var observer = new MutationObserver(mutations => { if ((mutations?.[0]?.addedNodes?.[0]?.tagName == "TR") || oldChartlist != document.querySelector(".chartlist")) { oldChartlist = document.querySelector(".chartlist"); if (!document.querySelector(".edit-selected-scrobbles-btn")) main(); } }); observer.observe(document.querySelector("body"), { childList: true, subtree: true }); } function addEditButtonToMenu(){ var moreMenu = document.querySelectorAll(".chartlist-more-menu"); moreMenu.forEach((menu) => { var fourteenDaysAgo = new Date().getTime() - (14 * 24 * 60 * 60 * 1000); if ((menu.querySelector('[name="timestamp"]')?.value ?? 0) * 1000 < fourteenDaysAgo) return; var listItem = document.createElement("li"); var editButton = document.createElement("button"); var editIcon = document.createElement("img"); editButton.className = "mimic-link dropdown-menu-clickable-item edit-selected-scrobbles-btn"; editButton.addEventListener("click", addInput); editIcon.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABQElEQVQ4T5WSMS9EQRRGdxOFYolCI/EDlCQKjYROo1NQSAii1SusSqsTUVDQKRSKFRKFQkm5BZ2eQqHjnJc7ycvbeSv7kpM3c2e+796Zuc3GYN8k27dgGq7gujmAfoq9jzAMXZiDTQ3G4AhWY5w8XxnMxCSJ3WtMg3toaXAC23ABH6WK3hlfQlls9mM4hSfoaPDpWWAnc5wkdmkRdmEPfuAL5jX4hTYcVgyqYss29hJiDbt1BgssHsBo/G/5p5j3YLIbE+YMhog/g09WZInMvoBfihWTnIHPo8E6lC+xR1xnYOltmAAb5rySeYX5PhRPnKvA7LPgTbfgDZbjKGpSgqIJ6wzGWevAHTyEWRTyv0HaWPfvqaBfI+VMzgja9iPpCLbyBnjj5VbOiX1axfbFWjKwMXRdikvrd4TvENv2jht/CXpR/3sr35MAAAAASUVORK5CYII="; editIcon.style = "padding-right: 14px;"; editButton.appendChild(editIcon); editButton.appendChild(document.createTextNode("Edit scrobble")); listItem.appendChild(editButton); menu.insertBefore(listItem, menu.firstChild); }); } function addInput(){ var editButton = this; var trackinfo = editButton.closest("tr"); addInputContainer(trackinfo, "artist"); var nameContainer = addInputContainer(trackinfo, "name"); nameContainer.style = "margin-left: 0.5em; margin-right: 8em;"; trackinfo.querySelector(".chartlist-buylinks").style = "display:none"; trackinfo.querySelector(".chartlist-more-button").style = "display:none"; var saveContainer = document.createElement("td"); saveContainer.style = "margin-left: 1em; margin-right: -2.4em; z-index: 1; opacity: 0.3;"; saveContainer.classList.add("chartlist-save"); var saveButton = document.createElement("button"); saveButton.addEventListener("click", scrobbleSong); saveButton.style = `padding: 8px 7px; margin: 8px; background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAQCAYAAAAmlE46AAAA2UlEQVQoU2NkIBMwAvWZAPFkILbAY8YfoNwaIC4E4hcgdSCNx4FYACrgAKQb0AwAiYHwFyB+AsSOILUgjf+RFIM0gcSQQT1U3hZIb4dpJkUjSK0NVPMWZI1/gYLNaLYhc2EuaQcKFiBrnAsUSABiZiyaHwLFFkDFwU5H1tiIxzYMP+PSCIsikIZSID6CpBOvjeuBCgOgircBaW9iNW4CKvSFKt4BpD2J1QgK9n6o4kogvYdYjfjCCe5HWJJbQWSoZgDVPYGlBpCzQCFJDDgDVFSJni6J0QhWAwAcqzvPSVZJyQAAAABJRU5ErkJggg==");`; saveContainer.appendChild(saveButton); trackinfo.insertBefore(saveContainer, trackinfo.querySelector(".chartlist-timestamp")); var style = document.createElement("style"); style.textContent = ".chartlist-save:hover { opacity: 1.0! important; }"; document.head.appendChild(style); } function addInputContainer(trackinfo, containerName){ var container = trackinfo.querySelector(".chartlist-" + containerName); var oldInput = container.firstElementChild.title; var inputElement = document.createElement("input"); inputElement.value = oldInput; inputElement.classList.add(containerName + "-input"); container.firstElementChild.style = "display:none;"; container.appendChild(inputElement); container.classList.remove("chartlist-" + containerName); container.classList.add("chartlist" + containerName); inputElement.addEventListener("keydown", function(event) { if (event.key === "Enter") { scrobbleSong.bind(trackinfo)(); } else if (event.key === "Escape"){ removeInput(trackinfo); } }); return container; } function scrobbleSong(){ var trackinfo = this.closest("tr"); var artist = encodeURIComponent(trackinfo.querySelector(".artist-input").value); var track = encodeURIComponent(trackinfo.querySelector(".name-input").value); var timestamp = trackinfo.querySelector('[name="timestamp"]').value; var oldTrack = encodeURIComponent(trackinfo.querySelector(".chartlistname > a").title); var oldArtist = encodeURIComponent(trackinfo.querySelector(".chartlistartist > a").title); if (artist.toLowerCase() == oldArtist.toLowerCase() && track.toLowerCase() == oldTrack.toLowerCase()) { removeInput(trackinfo); return; } var data = "api_key=" + api_key + "&sk=" + sessionKey + "&method=track.scrobble&artist=" + artist + "&track=" + track + "×tamp=" + timestamp; GM.xmlHttpRequest({ method: "POST", url: "https://ws.audioscrobbler.com/2.0/", headers: {"Content-Type": "application/x-www-form-urlencoded"}, data: data + "&api_sig=" + lfmmd5(data), onload: function(response) { if (response.responseText.length > 0 && response.responseText.includes('<lfm status="ok">')) { trackinfo.querySelector(".more-item--delete").click(); removeInput(trackinfo, decodeURIComponent(artist), decodeURIComponent(track)); setTimeout(function() {location.reload();}, 300); } else if (response.responseText.includes('<error code="9">')) { localStorage.removeItem("sessionKey"); authenticate(); } else { console.log("Error from Last.fm: " + response.responseText); } }, onerror: function(response) { console.log("Error in fetching contents: " + response.responseText); } }); } function removeInput(trackinfo, artist = null, track = null){ trackinfo.style = "opacity: 1.0;"; removeInputContainer(trackinfo, track, "name"); removeInputContainer(trackinfo, artist, "artist"); trackinfo.querySelector(".chartlist-buylinks").style = "display:initial"; trackinfo.querySelector(".chartlist-more-button").style = "display:initial"; trackinfo.querySelector(".chartlist-save").remove(); } function removeInputContainer(trackinfo, newInput, containerName){ var container = trackinfo.querySelector(".chartlist" + containerName); container.querySelector("." + containerName + "-input").remove(); container.classList.remove("chartlist" + containerName); container.classList.add("chartlist-" + containerName); container.style = ""; var linkElement = container.firstElementChild; linkElement.style = "display:initial;"; if (newInput){ linkElement.title = newInput; linkElement.innerHTML = newInput; } } function lfmmd5(f){for(var k=[],i=0;64>i;)k[i]=0|4294967296*Math.sin(++i%Math.PI);var c,d,e,h=[c=1732584193,d=4023233417,~c,~d],g=[],b=decodeURIComponent(unescape(f=f.split("&").sort().join("").replace(/=/g,"")+atob("ZmY4MmMzNTkzZWI3Zjg5OGMzMjhjZmIwN2JiNjk2ZWM=")))+"\u0080",a=b.length;f=--a/4+2|15;for(g[--f]=8*a;~a;)g[a>>2]|=b.charCodeAt(a)<<8*a--;for(i=b=0;i<f;i+=16){for(a=h;64>b;a=[e=a[3],c+((e=a[0]+[c&d|~c&e,e&c|~e&d,c^d^e,d^(c|~e)][a=b>>4]+k[b]+~~g[i|[b,5*b+1,3*b+5,7*b][a]&15])<<(a=[7,12,17,22,5,9,14,20,4,11,16,23,6,10,15,21][4*a+b++%4])|e>>>-a),c,d])c=a[1]|0,d=a[2];for(b=4;b;)h[--b]+=a[b]}for(f="";32>b;)f+=(h[b>>3]>>4*(1^b++)&15).toString(16);return f;}; })();