// ==UserScript==
// @name scRYMble
// @version 2.20240707021902
// @license MIT
// @description Visit a release page on rateyourmusic.com and scrobble the songs you see!
// @author fidwell
// @icon https://e.snmc.io/2.5/img/sonemic.png
// @namespace https://github.com/fidwell/scRYMble
// @include https://rateyourmusic.com/release/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @require http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js
// @require https://update.greasyfork.icu/scripts/130/10066/Portable%20MD5%20Function.js
// @downloadURL none
// ==/UserScript==
'use strict';
var ScrobbleRecord = /** @class */ (function () {
function ScrobbleRecord(trackName, artist, duration) {
this.artist = artist;
this.trackName = trackName;
var durastr = duration.trim();
var colon = durastr.indexOf(":");
if (colon !== -1) {
var minutes = parseInt(durastr.substring(0, colon));
var seconds = parseInt(durastr.substring(colon + 1));
this.duration = minutes * 60 + seconds;
}
else {
this.duration = 180;
}
this.time = 0;
}
return ScrobbleRecord;
}());
var HttpResponse = /** @class */ (function () {
function HttpResponse(raw) {
this.status = raw.status;
this.statusText = raw.statusText;
this.responseText = raw.responseText;
this.lines = raw.responseText.split("\n");
}
Object.defineProperty(HttpResponse.prototype, "isOkStatus", {
get: function () {
return this.lines[0] === "OK";
},
enumerable: false,
configurable: true
});
Object.defineProperty(HttpResponse.prototype, "sessionId", {
get: function () {
return this.lines[1];
},
enumerable: false,
configurable: true
});
Object.defineProperty(HttpResponse.prototype, "nowPlayingUrl", {
get: function () {
return this.lines[2];
},
enumerable: false,
configurable: true
});
Object.defineProperty(HttpResponse.prototype, "submitUrl", {
get: function () {
return this.lines[3];
},
enumerable: false,
configurable: true
});
return HttpResponse;
}());
function httpGet(url, onload) {
GM_xmlhttpRequest({
method: "GET",
url: url,
headers: {
"User-agent": "Mozilla/4.0 (compatible) Greasemonkey"
},
onload: function (responseRaw) { return onload(new HttpResponse(responseRaw)); }
});
}
function httpPost(url, data, onload) {
GM_xmlhttpRequest({
method: "POST",
url: url,
data: data,
headers: {
"User-agent": "Mozilla/4.0 (compatible) Greasemonkey",
"Content-type": "application/x-www-form-urlencoded"
},
onload: function (responseRaw) { return onload(new HttpResponse(responseRaw)); }
});
}
function fetch_unix_timestamp() {
return parseInt(new Date().getTime().toString().substring(0, 10));
}
function handshake(callback) {
var _a, _b, _c, _d;
var user = (_b = (_a = $("#scrobbleusername").val()) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : "";
var password = (_d = (_c = $("#scrobblepassword").val()) === null || _c === void 0 ? void 0 : _c.toString()) !== null && _d !== void 0 ? _d : "";
GM_setValue("user", user);
GM_setValue("pass", password);
var timestamp = fetch_unix_timestamp();
var auth = hex_md5("".concat(hex_md5(password)).concat(timestamp));
var handshakeURL = "http://post.audioscrobbler.com/?hs=true&p=1.2&c=scr&v=1.0&u=".concat(user, "&t=").concat(timestamp, "&a=").concat(auth);
httpGet(handshakeURL, callback);
}
var scRYMbleUi = /** @class */ (function () {
function scRYMbleUi() {
this.enabled = false;
this.eleTrackTable = $("#tracks");
if (this.eleTrackTable.children().length === 0) {
console.log("scRYMble: No track list found.");
}
else {
this.enabled = true;
this.createCheckboxes();
this.createControls();
}
}
Object.defineProperty(scRYMbleUi.prototype, "isEnabled", {
get: function () {
return this.enabled;
},
enumerable: false,
configurable: true
});
scRYMbleUi.prototype.createCheckboxes = function () {
var n = 0;
var chkbox = "";
$.each($("#tracks > .track > .tracklist_line"), function () {
if ($(this).find(".tracklist_num:eq(0)").text() !== "\n \n ") {
n++;
$(this).prepend(chkbox.replace("NUM", "".concat(n)));
}
});
};
scRYMbleUi.prototype.createControls = function () {
var _this = this;
var eleButtonDiv = document.createElement("div");
eleButtonDiv.innerHTML = "
";
eleButtonDiv.style.textAlign = "right";
this.eleTrackTable.after(eleButtonDiv);
var eleAllOrNone = document.getElementById("allornone");
eleAllOrNone === null || eleAllOrNone === void 0 ? void 0 : eleAllOrNone.addEventListener("click", function () { return _this.allOrNoneClick(); }, true);
};
scRYMbleUi.prototype.hookUpScrobbleNow = function (startScrobble) {
var eleScrobbleNow = document.getElementById("scrobblenow");
eleScrobbleNow === null || eleScrobbleNow === void 0 ? void 0 : eleScrobbleNow.addEventListener("click", startScrobble, true);
};
scRYMbleUi.prototype.hookUpScrobbleThen = function (handshakeBatch) {
var _a;
(_a = document.getElementById("scrobblethen")) === null || _a === void 0 ? void 0 : _a.addEventListener("click", handshakeBatch, true);
};
scRYMbleUi.prototype.setMarquee = function (value) {
var marquee = document.getElementById("scrymblemarquee");
if (marquee !== null) {
marquee.innerHTML = value;
}
};
scRYMbleUi.prototype.setProgressBar = function (percentage) {
var progbar = document.getElementById("progbar");
if (progbar !== null && percentage >= 0 && percentage <= 100) {
progbar.style.width = "".concat(percentage, "%");
}
};
scRYMbleUi.prototype.allOrNoneClick = function () {
var _this = this;
window.setTimeout(function () { return _this.allOrNoneAction(); }, 10);
};
scRYMbleUi.prototype.allOrNoneAction = function () {
$.each($(".scrymblechk"), function () {
$(this).prop("checked", $("#allornone").is(":checked"));
});
};
scRYMbleUi.prototype.elementsOnAndOff = function (state) {
$("#scrobblenow").prop("disabled", !state);
$("#scrobblepassword").prop("disabled", !state);
$("#scrobbleusername").prop("disabled", !state);
$("#scrobblepassword").prop("disabled", !state);
$.each($(".scrymblechk"), function () {
try {
$(this).prop("disabled", !state);
}
catch (e) {
console.log(e);
}
});
};
scRYMbleUi.prototype.elementsOff = function () {
this.elementsOnAndOff(false);
};
scRYMbleUi.prototype.elementsOn = function () {
this.elementsOnAndOff(true);
};
return scRYMbleUi;
}());
var ui = new scRYMbleUi();
var toScrobble = [];
var currentlyScrobbling = -1;
var sessID = "";
var submitURL = "";
var npURL = "";
var currTrackDuration = 0;
var currTrackPlayTime = 0;
function confirmBrowseAway(oEvent) {
if (currentlyScrobbling !== -1) {
oEvent.preventDefault();
return "You are currently scrobbling a record. Leaving the page now will prevent future tracks from this release from scrobbling.";
}
return "";
}
function getPageArtist() {
var byartist = $("span[itemprop=\"byArtist\"]");
var art_cred = $(byartist).find(".credited_name:eq(0) > span[itemprop=\"name\"]");
if ($(art_cred).length > 0) {
return $(art_cred).text();
}
else {
return $(byartist).text();
}
}
function getAlbum() {
var _a, _b;
return (_b = (_a = $(".release_page meta[itemprop=\"name\"]").attr("content")) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : "";
}
function isVariousArtists() {
var artist = getPageArtist();
return artist.indexOf("Various Artists") > -1 ||
artist.indexOf(" / ") > -1;
}
function acceptSubmitResponse(responseDetails, isBatch) {
if (responseDetails.status === 200) {
if (!responseDetails.isOkStatus) {
alertSubmitFailed(responseDetails);
}
}
else {
alertSubmitFailed(responseDetails);
}
if (isBatch) {
ui.setMarquee("Scrobbled OK!");
}
else {
scrobbleNextSong();
}
}
function alertSubmitFailed(responseDetails) {
alert("Track submit failed: ".concat(responseDetails.status, " ").concat(responseDetails.statusText, "\n\nData:\n").concat(responseDetails.responseText));
}
function acceptSubmitResponseSingle(responseDetails) {
acceptSubmitResponse(responseDetails, false);
}
function acceptSubmitResponseBatch(responseDetails) {
acceptSubmitResponse(responseDetails, true);
}
function acceptNPResponse(responseDetails) {
if (responseDetails.status === 200) {
if (!responseDetails.isOkStatus) {
alertSubmitFailed(responseDetails);
}
}
else {
alertSubmitFailed(responseDetails);
}
}
function buildListOfSongsToScrobble() {
toScrobble = [];
$.each($(".scrymblechk"), function () {
if ($(this).is(":checked")) {
var song = $(this).parent().parent();
var songTitle = $(song).find("span[itemprop=\"name\"]").text();
var artist = getPageArtist();
var length_1 = $(song).find(".tracklist_duration").text();
if (isVariousArtists()) {
var firstDash = songTitle.indexOf(" - ");
if (firstDash === -1) {
// no dash exists! must be a single artist with " / " in the name or v/a with unscrobbleable list
artist = getPageArtist();
if (artist.indexOf("Various Artists") > -1) {
artist = $(".album_title:eq(0)").text();
}
}
else {
artist = songTitle.substring(0, firstDash);
songTitle = songTitle.substring(firstDash + 3);
}
}
else {
artist = getPageArtist();
var title = $(song).find("span[itemprop=\"name\"]");
if ($(title).html().indexOf(" 0) {
var firstDash = songTitle.indexOf(" - ");
artist = songTitle.substring(0, firstDash);
songTitle = songTitle.substring(firstDash + 3);
}
}
if (songTitle.toLowerCase() === "untitled" ||
songTitle.toLowerCase() === "untitled track" ||
songTitle === "") {
songTitle = "[untitled]";
}
while (songTitle.indexOf(" ") > 0) {
songTitle = songTitle.replace(" ", " ");
}
toScrobble[toScrobble.length] = new ScrobbleRecord(songTitle, artist, length_1);
}
});
}
function submitTracksBatch(sessID, submitURL) {
buildListOfSongsToScrobble();
if (toScrobble === null)
return;
var currTime = fetch_unix_timestamp();
var hoursFudgeStr = prompt("How many hours ago did you listen to this?");
if (hoursFudgeStr !== null) {
var album = getAlbum();
var hoursFudge = parseFloat(hoursFudgeStr);
if (!isNaN(hoursFudge)) {
currTime = currTime - hoursFudge * 60 * 60;
}
for (var i = toScrobble.length - 1; i >= 0; i--) {
currTime = currTime * 1 - toScrobble[i].duration * 1;
toScrobble[i].time = currTime;
}
var outstr = "Artist: ".concat(getPageArtist(), "\nAlbum: ").concat(album, "\n");
for (var _i = 0, toScrobble_1 = toScrobble; _i < toScrobble_1.length; _i++) {
var song = toScrobble_1[_i];
outstr = "".concat(outstr).concat(song.trackName, " (").concat(song.duration, ")\n");
}
var postdata = {};
for (var i = 0; i < toScrobble.length; i++) {
postdata["a[".concat(i, "]")] = toScrobble[i].artist;
postdata["t[".concat(i, "]")] = toScrobble[i].trackName;
postdata["b[".concat(i, "]")] = album;
postdata["n[".concat(i, "]")] = "".concat(i + 1);
postdata["l[".concat(i, "]")] = "".concat(toScrobble[i].duration);
postdata["i[".concat(i, "]")] = "".concat(toScrobble[i].time);
postdata["o[".concat(i, "]")] = "P";
postdata["r[".concat(i, "]")] = "";
postdata["m[".concat(i, "]")] = "";
}
postdata["s"] = sessID;
var postdataStr = "";
var firstTime = true;
for (var currKey in postdata) {
if (firstTime) {
firstTime = false;
}
else {
postdataStr = "".concat(postdataStr, "&");
}
postdataStr = "".concat(postdataStr).concat(encodeURIComponent(currKey), "=").concat(encodeURIComponent(postdata[currKey]));
}
httpPost(submitURL, postdataStr, acceptSubmitResponseBatch);
}
}
function startScrobble() {
currentlyScrobbling = -1;
currTrackDuration = 0;
currTrackPlayTime = 0;
ui.elementsOff();
buildListOfSongsToScrobble();
scrobbleNextSong();
}
function resetScrobbler() {
currentlyScrobbling = -1;
currTrackDuration = 0;
currTrackPlayTime = 0;
ui.setMarquee(" ");
ui.setProgressBar(0);
toScrobble = [];
ui.elementsOn();
}
function scrobbleNextSong() {
currentlyScrobbling++;
if (currentlyScrobbling === toScrobble.length) {
resetScrobbler();
}
else {
window.setTimeout(timertick, 10);
handshake(acceptHandshakeSingle);
}
}
function submitThisTrack() {
var postdata = {};
var i = 0;
var currTime = fetch_unix_timestamp();
postdata["a".concat(i)] = toScrobble[currentlyScrobbling].artist;
postdata["t".concat(i)] = toScrobble[currentlyScrobbling].trackName;
postdata["b".concat(i)] = getAlbum();
postdata["n".concat(i)] = "".concat(currentlyScrobbling + 1);
postdata["l".concat(i)] = "".concat(toScrobble[currentlyScrobbling].duration);
postdata["i".concat(i)] = "".concat(currTime - toScrobble[currentlyScrobbling].duration);
postdata["o".concat(i)] = "P";
postdata["r".concat(i)] = "";
postdata["m".concat(i)] = "";
postdata["s"] = sessID;
var postdataStr = "";
var firstTime = true;
for (var currKey in postdata) {
if (firstTime) {
firstTime = false;
}
else {
postdataStr = "".concat(postdataStr, "&");
}
postdataStr = "".concat(postdataStr).concat(encodeURIComponent(currKey), "=").concat(encodeURIComponent(postdata[currKey]));
}
httpPost(submitURL, postdataStr, acceptSubmitResponseSingle);
}
function npNextTrack() {
var postdata = {};
postdata["a"] = toScrobble[currentlyScrobbling].artist;
postdata["t"] = toScrobble[currentlyScrobbling].trackName;
postdata["b"] = getAlbum();
postdata["n"] = "".concat(currentlyScrobbling + 1);
postdata["l"] = "".concat(toScrobble[currentlyScrobbling].duration);
postdata["m"] = "";
postdata["s"] = sessID;
currTrackDuration = toScrobble[currentlyScrobbling].duration;
currTrackPlayTime = 0;
ui.setMarquee(toScrobble[currentlyScrobbling].trackName);
var postdataStr = "";
var firstTime = true;
for (var currKey in postdata) {
if (firstTime) {
firstTime = false;
}
else {
postdataStr = "".concat(postdataStr, "&");
}
postdataStr = postdataStr + encodeURIComponent(currKey) + "=" + encodeURIComponent(postdata[currKey]);
}
httpPost(npURL, postdataStr, acceptNPResponse);
}
function timertick() {
var again = true;
if (currentlyScrobbling !== -1) {
if (currTrackDuration !== 0) {
ui.setProgressBar(100 * currTrackPlayTime / currTrackDuration);
}
currTrackPlayTime++;
if (currTrackPlayTime === currTrackDuration) {
submitThisTrack();
again = false;
}
}
if (again) {
window.setTimeout(timertick, 1000);
}
}
function acceptHandshakeSingle(responseDetails) {
acceptHandshake(responseDetails, false);
}
function acceptHandshakeBatch(responseDetails) {
acceptHandshake(responseDetails, true);
}
function acceptHandshake(responseDetails, isBatch) {
if (responseDetails.status === 200) {
if (!responseDetails.isOkStatus) {
alertHandshakeFailed(responseDetails);
}
else {
sessID = responseDetails.sessionId;
npURL = responseDetails.nowPlayingUrl;
submitURL = responseDetails.submitUrl;
if (isBatch) {
submitTracksBatch(sessID, submitURL);
}
else {
npNextTrack();
}
}
}
else {
alertHandshakeFailed(responseDetails);
}
}
function alertHandshakeFailed(responseDetails) {
alert("Handshake failed: ".concat(responseDetails.status, " ").concat(responseDetails.statusText, "\n\nData:\n").concat(responseDetails.responseText));
}
function handshakeBatch() {
handshake(acceptHandshakeBatch);
}
(function () {
if (!ui.isEnabled) {
return;
}
ui.hookUpScrobbleNow(startScrobble);
ui.hookUpScrobbleThen(handshakeBatch);
window.addEventListener("beforeunload", confirmBrowseAway, true);
})();