Visit a release page on rateyourmusic.com and scrobble the songs you see!
// ==UserScript==
// @name scRYMble
// @license MIT
// @version 2.20260823020556
// @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_deleteValue
// @grant GM_xmlhttpRequest
// ==/UserScript==
'use strict';
class HttpResponseRaw {
constructor() {
this.status = 0;
this.statusText = "";
this.responseText = "";
this.responseHeaders = "";
}
}
function secureUrl(url) {
return url
.replace(/^http:\/\//i, "https://")
.replace(/^(https:\/\/[^/:]+):80(?=\/|$)/i, "$1");
}
class HttpResponse {
constructor(raw) {
this.status = raw.status;
this.statusText = raw.statusText;
this.responseText = raw.responseText;
this.responseHeaders = raw.responseHeaders;
this.lines = raw.responseText.split("\n");
}
serverTimeMs() {
const match = this.responseHeaders.match(/^date:\s*(.+)$/im);
if (!match) {
return null;
}
const parsedMs = new Date(match[1].trim()).getTime();
return isNaN(parsedMs) ? null : parsedMs;
}
line(index) {
var _a;
return (_a = this.lines[index]) !== null && _a !== void 0 ? _a : "";
}
get isOkStatus() {
return this.lines[0] === "OK";
}
get sessionId() {
return this.line(1);
}
get nowPlayingUrl() {
return secureUrl(this.line(2));
}
get submitUrl() {
return secureUrl(this.line(3));
}
}
const REQUEST_TIMEOUT_MS = 30000;
function encodeParams(params) {
return Object.entries(params)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join("&");
}
function httpGet(url, onload, onerror) {
GM_xmlhttpRequest({
method: "GET",
url,
headers: {
"User-agent": "Mozilla/4.0 (compatible) Greasemonkey"
},
timeout: REQUEST_TIMEOUT_MS,
onload: (responseRaw) => onload(new HttpResponse(responseRaw)),
onerror: (responseRaw) => onerror(responseRaw),
ontimeout: () => onerror(new HttpResponseRaw())
});
}
function httpPost(url, data, onload, onerror) {
GM_xmlhttpRequest({
method: "POST",
url,
data,
headers: {
"User-agent": "Mozilla/4.0 (compatible) Greasemonkey",
"Content-type": "application/x-www-form-urlencoded"
},
timeout: REQUEST_TIMEOUT_MS,
onload: (responseRaw) => onload(new HttpResponse(responseRaw)),
onerror: (responseRaw) => onerror(responseRaw),
ontimeout: () => onerror(new HttpResponseRaw())
});
}
// Vendored MD5 implementation (RFC 1321), written directly from the
// algorithm description. Replaces the third-party script previously pulled
// at install time via @require ("Portable MD5 Function" on greasyfork.org),
// so the userscript no longer loads remote code.
const SHIFTS = [
7, 12, 17, 22,
5, 9, 14, 20,
4, 11, 16, 23,
6, 10, 15, 21
];
const SINE_CONSTANTS = Array.from({ length: 64 }, (_, i) => Math.floor(Math.abs(Math.sin(i + 1)) * 4294967296));
function hex_md5(input) {
return md5Words(new TextEncoder().encode(input)).map(hexWord).join("");
}
function md5Words(bytes) {
const padded = padMessage(bytes);
let a0 = 0x67452301;
let b0 = 0xEFCDAB89;
let c0 = 0x98BADCFE;
let d0 = 0x10325476;
for (let offset = 0; offset < padded.length; offset += 64) {
const words = littleEndianWords(padded, offset);
let a = a0;
let b = b0;
let c = c0;
let d = d0;
for (let i = 0; i < 64; i++) {
let f;
let messageIndex;
if (i < 16) {
f = b & c | ~b & d;
messageIndex = i;
}
else if (i < 32) {
f = d & b | ~d & c;
messageIndex = (5 * i + 1) % 16;
}
else if (i < 48) {
f = b ^ c ^ d;
messageIndex = (3 * i + 5) % 16;
}
else {
f = c ^ (b | ~d);
messageIndex = 7 * i % 16;
}
f = f + a + SINE_CONSTANTS[i] + words[messageIndex] | 0;
a = d;
d = c;
c = b;
b = b + rotateLeft(f, SHIFTS[i % 4 + Math.floor(i / 16) * 4]) | 0;
}
a0 = a0 + a | 0;
b0 = b0 + b | 0;
c0 = c0 + c | 0;
d0 = d0 + d | 0;
}
return [a0, b0, c0, d0];
}
function padMessage(bytes) {
const bitLength = bytes.length * 8;
const paddedLength = (bytes.length + 8 >> 6) + 1 << 6;
const padded = new Uint8Array(paddedLength);
padded.set(bytes);
padded[bytes.length] = 0x80;
const lowBits = bitLength % 4294967296;
const highBits = Math.floor(bitLength / 4294967296);
for (let i = 0; i < 4; i++) {
padded[paddedLength - 8 + i] = lowBits >>> i * 8 & 0xFF;
padded[paddedLength - 4 + i] = highBits >>> i * 8 & 0xFF;
}
return padded;
}
function littleEndianWords(bytes, offset) {
const words = [];
for (let j = 0; j < 16; j++) {
const base = offset + j * 4;
words[j] =
bytes[base] |
bytes[base + 1] << 8 |
bytes[base + 2] << 16 |
bytes[base + 3] << 24;
}
return words;
}
function rotateLeft(value, shift) {
return value << shift | value >>> 32 - shift;
}
function hexWord(word) {
let result = "";
for (let byteIndex = 0; byteIndex < 4; byteIndex++) {
result += (word >>> byteIndex * 8 & 0xFF).toString(16).padStart(2, "0");
}
return result;
}
function fetch_unix_timestamp() {
return Math.floor(Date.now() / 1000);
}
function decodeHtmlEntities(value) {
const textarea = document.createElement("textarea");
textarea.innerHTML = value;
return textarea.value;
}
function stripAndClean(input) {
let result = decodeHtmlEntities(input)
.replace(/\n/g, " ")
.replace(/\u00A0/g, " ")
.replace(/ {2,}/g, " ")
.trim();
while (result.startsWith("& - ")) {
result = result.substring(4);
}
while (result.startsWith(" - ")) {
result = result.substring(3);
}
while (result.startsWith("- ")) {
result = result.substring(2);
}
return result;
}
const PASSWORD_HASH_KEY = "pwhash";
const LEGACY_PASSWORD_KEY = "pass";
const CLOCK_OFFSET_KEY = "clockOffsetSeconds";
function buildScrobbleParams(song, index, album, time) {
return {
[`a[${index}]`]: song.artist,
[`t[${index}]`]: song.trackName,
[`b[${index}]`]: album,
[`n[${index}]`]: `${index + 1}`,
[`l[${index}]`]: `${song.duration}`,
[`i[${index}]`]: `${time}`,
[`o[${index}]`]: "P",
[`r[${index}]`]: "",
[`m[${index}]`]: ""
};
}
function handshake(ui, callback, onError) {
const username = ui.username;
GM_setValue("user", username);
const passwordHash = resolveStoredHash(ui.password);
sendHandshake(username, passwordHash, callback, onError, true);
}
function sendHandshake(username, passwordHash, callback, onError, mayRetryForClockSkew) {
const timestamp = correctedUnixTimestamp();
const auth = hex_md5(`${passwordHash}${timestamp}`);
const handshakeURL = `https://post.audioscrobbler.com/?hs=true&p=1.2&c=scr&v=1.0&u=${encodeURIComponent(username)}&t=${timestamp}&a=${auth}`;
httpGet(handshakeURL, response => {
if (mayRetryForClockSkew && response.responseText.trim() === "BADTIME") {
learnClockOffset(response);
sendHandshake(username, passwordHash, callback, onError, false);
return;
}
callback(response);
}, onError);
}
function learnClockOffset(response) {
const serverTimeMs = response.serverTimeMs();
if (serverTimeMs !== null) {
GM_setValue(CLOCK_OFFSET_KEY, `${Math.round((serverTimeMs - Date.now()) / 1000)}`);
}
}
function correctedUnixTimestamp() {
const offsetSeconds = parseInt(GM_getValue(CLOCK_OFFSET_KEY, "0"), 10);
return fetch_unix_timestamp() + (isNaN(offsetSeconds) ? 0 : offsetSeconds);
}
function resolveStoredHash(typedPassword) {
let resolvedHash = "";
if (typedPassword.length > 0) {
resolvedHash = hex_md5(typedPassword);
GM_setValue(PASSWORD_HASH_KEY, resolvedHash);
}
else if (GM_getValue(PASSWORD_HASH_KEY, "").length > 0) {
resolvedHash = GM_getValue(PASSWORD_HASH_KEY, "");
}
else {
const legacyPassword = GM_getValue(LEGACY_PASSWORD_KEY, "");
if (legacyPassword.length > 0) {
resolvedHash = hex_md5(legacyPassword);
GM_setValue(PASSWORD_HASH_KEY, resolvedHash);
}
}
GM_deleteValue(LEGACY_PASSWORD_KEY);
return resolvedHash;
}
class rymUi {
constructor() {
this.albumTitleClass = ".album_title";
this.byArtistProperty = "byArtist";
this.creditedNameClass = "credited_name";
this.trackElementId = "tracks";
this.tracklistDurationClass = ".tracklist_duration";
this.tracklistLineClass = "tracklist_line";
this.tracklistNumClass = ".tracklist_num";
this.tracklistTitleClass = ".tracklist_title";
this.tracklistArtistClass = ".artist";
this.tracklistRenderedTextClass = ".rendered_text";
//#endregion
}
get isVariousArtists() {
const artist = this.pageArtist;
return artist.indexOf("Various Artists") > -1 ||
artist.indexOf(" / ") > -1;
}
get pageArtist() {
var _a;
return (_a = this.multipleByArtists) !== null && _a !== void 0 ? _a : this.singleByArtist;
}
get pageAlbum() {
var _a, _b;
// Not using innerText because it doesn't work with Jest tests.
const element = document.querySelector(this.albumTitleClass);
return ((_b = (_a = element.firstChild) === null || _a === void 0 ? void 0 : _a.textContent) !== null && _b !== void 0 ? _b : "").trim();
}
get multipleByArtists() {
return Array.from(document.getElementsByClassName(this.creditedNameClass))
.map(x => x)
.map(x => { var _a; return (_a = x.innerText) !== null && _a !== void 0 ? _a : ""; })[1];
}
get singleByArtist() {
return Array.from(document.querySelectorAll(`span[itemprop='${this.byArtistProperty}'] > a`))
.map(e => this.parseArtistLink(e))
.join(" / ");
}
parseArtistLink(element) {
return Array.from(element.childNodes)
.filter(node => node.nodeType === 3) // Node.TEXT_NODE
.map(node => { var _a, _b; return (_b = (_a = node.textContent) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : ""; })
.join("");
}
hasTrackNumber(tracklistLine) {
var _a, _b;
return ((_b = (_a = tracklistLine.querySelector(this.tracklistNumClass)) === null || _a === void 0 ? void 0 : _a.innerHTML) !== null && _b !== void 0 ? _b : "").trim().length > 0;
}
//#region Element getters
get trackListDiv() {
return document.getElementById(this.trackElementId);
}
get tracklistLines() {
var _a;
return Array.from((_a = this.trackListDiv.getElementsByClassName(this.tracklistLineClass)) !== null && _a !== void 0 ? _a : [])
.map(l => l);
}
tracklistLine(checkbox) {
var _a;
return (_a = checkbox.parentElement) === null || _a === void 0 ? void 0 : _a.parentElement;
}
trackName(tracklistLine) {
var _a, _b;
let songTitle = "";
const songTags = tracklistLine === null || tracklistLine === void 0 ? void 0 : tracklistLine.querySelectorAll("[itemprop=name]");
if (songTags.length > 0) {
const lastSongTag = songTags[songTags.length - 1];
songTitle = ((_a = lastSongTag === null || lastSongTag === void 0 ? void 0 : lastSongTag.textContent) !== null && _a !== void 0 ? _a : "").replace(/\n/g, " ");
// Check if the tag is hiding any artist links; if so, strip them out
const artistLinks = lastSongTag.querySelectorAll(this.tracklistArtistClass);
if (artistLinks.length > 0) {
const renderedTextSpan = lastSongTag.querySelector(this.tracklistRenderedTextClass);
songTitle = renderedTextSpan.innerHTML.replace(/<a[^>]*>.*?<\/a>/g, " ").trim();
}
}
else {
const renderedTextSpan = tracklistLine === null || tracklistLine === void 0 ? void 0 : tracklistLine.querySelector(this.tracklistRenderedTextClass);
songTitle = (_b = renderedTextSpan === null || renderedTextSpan === void 0 ? void 0 : renderedTextSpan.textContent) !== null && _b !== void 0 ? _b : "";
}
return stripAndClean(songTitle);
}
trackArtist(tracklistLine) {
var _a, _b;
const artistTags = tracklistLine === null || tracklistLine === void 0 ? void 0 : tracklistLine.querySelectorAll(this.tracklistArtistClass);
if (artistTags.length === 0)
return "";
if (artistTags.length === 1) {
return (_a = artistTags[0].textContent) !== null && _a !== void 0 ? _a : "";
}
// Multiple artists
const entireSpan = tracklistLine.querySelector(this.tracklistTitleClass);
const entireText = ((_b = entireSpan.textContent) !== null && _b !== void 0 ? _b : "").replace(/\n/g, " ");
const dashIndex = entireText.indexOf(" - ");
return entireText.substring(0, dashIndex);
}
trackDuration(tracklistLine) {
var _a;
const durationElement = tracklistLine === null || tracklistLine === void 0 ? void 0 : tracklistLine.querySelector(this.tracklistDurationClass);
return ((_a = durationElement.textContent) !== null && _a !== void 0 ? _a : "").trim();
}
}
class scRYMbleUi {
constructor(rymUi) {
var _a, _b;
this.enabled = false;
this.marqueeId = "scrymblemarquee";
this.progBarId = "progbar";
this.scrobbleNowId = "scrobblenow";
this.scrobbleThenId = "scrobblethen";
this.testId = "scrobbletest";
this.checkboxClass = "scrymblechk";
this.selectAllOrNoneId = "allornone";
this.usernameId = "scrobbleusername";
this.passwordId = "scrobblepassword";
this._rymUi = rymUi;
if (((_b = (_a = this._rymUi.trackListDiv) === null || _a === void 0 ? void 0 : _a.children.length) !== null && _b !== void 0 ? _b : 0) === 0) {
console.log("scRYMble: No track list found.");
}
else {
this.enabled = true;
this.createCheckboxes();
this.createControls();
}
}
get isEnabled() {
return this.enabled;
}
get username() {
return this.usernameInput.value;
}
get password() {
return this.passwordInput.value;
}
createCheckboxes() {
const checkboxTemplate = `<input type="checkbox" class="${this.checkboxClass}" checked="checked">`;
for (const tracklistLine of this._rymUi.tracklistLines) {
if (this._rymUi.hasTrackNumber(tracklistLine)) {
const thisCheckboxElement = document.createElement("span");
thisCheckboxElement.style.float = "left";
thisCheckboxElement.innerHTML = checkboxTemplate;
tracklistLine.prepend(thisCheckboxElement);
}
}
}
createControls() {
var _a;
const eleButtonDiv = document.createElement("div");
eleButtonDiv.innerHTML = `
<table style="border: 0;" cellpadding="0" cellspacing="2px">
<tr>
<td style="width: 112px;">
<input type="checkbox" name="${this.selectAllOrNoneId}" id="${this.selectAllOrNoneId}" style="vertical-align: middle;" checked="checked">
<label for="${this.selectAllOrNoneId}" style="font-size: 60%;">select all/none</label>
<br/>
<table border="2" cellpadding="0" cellspacing="0">
<tr>
<td style="height: 50px; width: 103px; background: url(https://cdn.last.fm/flatness/logo_black.3.png) no-repeat; color: #fff;">
<div class="marquee" style="position: relative; top: 17px; overflow: hidden; white-space: nowrap;">
<span style="font-size: 80%; width: 88px; display: inline-block; animation: marquee 5s linear infinite;" id="${this.marqueeId}"> </span>
</div>
</td>
</tr>
<tr>
<td style="background-color: #003;">
<div style="position: relative; background-color: #f00; width: 0; max-height: 5px; left: 0; top: 0;" id="${this.progBarId}"> </div>
</td>
</tr>
</table>
</td>
<td>user: <input type="text" size="16" id="${this.usernameId}" /><br />
pass: <input type="password" size="16" id="${this.passwordId}" /><br />
<input type="button" id="${this.scrobbleNowId}" value="Scrobble in real-time" />
<input type="button" id="${this.scrobbleThenId}" value="Scrobble a previous play" />
<input type="button" id="${this.testId}" value="Test tracklist parsing" style="display: none;" />
</td>
</tr>
</table>`;
eleButtonDiv.style.textAlign = "right";
(_a = this._rymUi.trackListDiv) === null || _a === void 0 ? void 0 : _a.after(eleButtonDiv);
this.usernameInput.value = GM_getValue("user", "");
if (GM_getValue(PASSWORD_HASH_KEY, "").length > 0 ||
GM_getValue(LEGACY_PASSWORD_KEY, "").length > 0) {
this.passwordInput.placeholder = "(saved)";
}
this.allOrNoneCheckbox.addEventListener("click", () => this.allOrNoneClick(), true);
const marqueeStyle = document.createElement("style");
document.head.appendChild(marqueeStyle);
marqueeStyle.textContent = `
@keyframes marquee {
0% { transform: translateX(100%); }
100% { transform: translateX(-100%); }
}`;
}
hookUpScrobbleNow(startScrobble) {
this.scrobbleNowButton.addEventListener("click", startScrobble, true);
}
hookUpScrobbleThen(handshakeBatch) {
this.scrobbleThenButton.addEventListener("click", handshakeBatch, true);
}
hookUpScrobbleTest(callback) {
this.scrobbleTestButton.addEventListener("click", callback, true);
}
setMarquee(value) {
this.marquee.innerHTML = value;
}
setProgressBar(percentage) {
if (percentage >= 0 && percentage <= 100) {
this.progressBar.style.width = `${percentage}%`;
}
}
allOrNoneClick() {
window.setTimeout(() => this.allOrNoneAction(), 10);
}
allOrNoneAction() {
for (const checkbox of this.checkboxes) {
checkbox.checked = this.allOrNoneCheckbox.checked;
}
}
elementsOnAndOff(state) {
const controls = [
this.scrobbleNowButton,
this.scrobbleThenButton,
this.usernameInput,
this.passwordInput
];
for (const control of controls) {
control.toggleAttribute("disabled", !state);
}
for (const checkbox of this.checkboxes) {
checkbox.toggleAttribute("disabled", !state);
}
}
elementsOff() {
this.elementsOnAndOff(false);
}
elementsOn() {
this.elementsOnAndOff(true);
}
//#region Element getters
get allOrNoneCheckbox() {
return document.getElementById(this.selectAllOrNoneId);
}
get scrobbleNowButton() {
return document.getElementById(this.scrobbleNowId);
}
get scrobbleThenButton() {
return document.getElementById(this.scrobbleThenId);
}
get scrobbleTestButton() {
return document.getElementById(this.testId);
}
get marquee() {
return document.getElementById(this.marqueeId);
}
get progressBar() {
return document.getElementById(this.progBarId);
}
get usernameInput() {
return document.getElementById(this.usernameId);
}
get passwordInput() {
return document.getElementById(this.passwordId);
}
get checkboxes() {
return document.getElementsByClassName(this.checkboxClass);
}
}
class ScrobbleRecord {
constructor(trackName, artist, duration) {
this.artist = artist;
this.trackName = trackName;
const durastr = duration.trim();
if (durastr.indexOf(":") !== -1) {
this.duration = durastr
.split(":")
.reduce((totalSeconds, part) => totalSeconds * 60 + parseInt(part), 0);
}
else {
this.duration = 180;
}
this.time = 0;
}
}
function buildListOfSongsToScrobble(_rymUi, _scRYMbleUi) {
const toScrobble = [];
Array.from(_scRYMbleUi.checkboxes).forEach(checkbox => {
if (checkbox.checked) {
toScrobble[toScrobble.length] = parseTracklistLine(_rymUi, checkbox);
}
});
return toScrobble;
}
function parseTracklistLine(rymUi, checkbox) {
const tracklistLine = rymUi.tracklistLine(checkbox);
const pageArtist = rymUi.pageArtist;
let songTitle = rymUi.trackName(tracklistLine);
let artist = pageArtist;
const duration = rymUi.trackDuration(tracklistLine);
if (rymUi.isVariousArtists) {
artist = rymUi.trackArtist(tracklistLine);
if (artist.length === 0) {
artist = pageArtist.indexOf("Various Artists") > -1
? rymUi.pageAlbum
: pageArtist; // Probably a collaboration release, like a classical work.
}
}
else {
const trackArtist = rymUi.trackArtist(tracklistLine);
if (trackArtist.length > 0) {
artist = trackArtist;
}
}
if (songTitle.toLowerCase() === "untitled" ||
songTitle.toLowerCase() === "untitled track" ||
songTitle === "") {
songTitle = "[untitled]";
}
return new ScrobbleRecord(songTitle, artist, duration);
}
const _rymUi = new rymUi();
const _scRYMbleUi = new scRYMbleUi(_rymUi);
let toScrobble = [];
let currentlyScrobbling = -1;
let sessID = "";
let submitURL = "";
let npURL = "";
let currTrackDuration = 0;
let 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 acceptSubmitResponse(responseDetails, isBatch) {
if (!responseDetails.isOkStatus) {
alertRequestFailed(responseDetails);
resetScrobbler();
return;
}
if (isBatch) {
_scRYMbleUi.elementsOn();
_scRYMbleUi.setMarquee("Scrobbled OK!");
}
else {
scrobbleNextSong();
}
}
function alertRequestFailed(responseDetails) {
alert(`Track submit failed: ${responseDetails.status} ${responseDetails.statusText}\n\nData:\n${responseDetails.responseText}`);
}
function acceptSubmitResponseSingle(responseDetails) {
acceptSubmitResponse(responseDetails, false);
}
function acceptSubmitResponseBatch(responseDetails) {
acceptSubmitResponse(responseDetails, true);
}
function acceptNPResponse(responseDetails) {
if (!responseDetails.isOkStatus) {
alertRequestFailed(responseDetails);
}
}
function submitTracksBatch() {
toScrobble = buildListOfSongsToScrobble(_rymUi, _scRYMbleUi);
let currTime = fetch_unix_timestamp();
const hoursFudgeStr = prompt("How many hours ago did you finish listening to this?");
if (hoursFudgeStr === null) {
_scRYMbleUi.elementsOn();
return;
}
const album = _rymUi.pageAlbum;
const hoursFudge = parseFloat(hoursFudgeStr);
if (!isNaN(hoursFudge)) {
currTime = currTime - hoursFudge * 60 * 60;
}
for (let i = toScrobble.length - 1; i >= 0; i--) {
currTime -= toScrobble[i].duration;
toScrobble[i].time = currTime;
}
let outstr = `Artist: ${_rymUi.pageArtist}\nAlbum: ${album}\n`;
for (const song of toScrobble) {
outstr = `${outstr}${song.trackName} (${song.duration})\n`;
}
const postdata = {};
for (let i = 0; i < toScrobble.length; i++) {
Object.assign(postdata, buildScrobbleParams(toScrobble[i], i, album, toScrobble[i].time));
}
postdata["s"] = sessID;
httpPost(submitURL, encodeParams(postdata), acceptSubmitResponseBatch, handleNetworkError);
}
function startScrobble() {
currentlyScrobbling = -1;
currTrackDuration = 0;
currTrackPlayTime = 0;
_scRYMbleUi.elementsOff();
toScrobble = buildListOfSongsToScrobble(_rymUi, _scRYMbleUi);
scrobbleNextSong();
}
function resetScrobbler() {
currentlyScrobbling = -1;
currTrackDuration = 0;
currTrackPlayTime = 0;
_scRYMbleUi.setMarquee(" ");
_scRYMbleUi.setProgressBar(0);
toScrobble = [];
_scRYMbleUi.elementsOn();
}
function scrobbleNextSong() {
currentlyScrobbling++;
if (currentlyScrobbling === toScrobble.length) {
resetScrobbler();
}
else {
window.setTimeout(timertick, 10);
handshake(_scRYMbleUi, acceptHandshakeSingle, handleNetworkError);
}
}
function submitThisTrack() {
const song = toScrobble[currentlyScrobbling];
const currTime = fetch_unix_timestamp();
const postdata = buildScrobbleParams(song, currentlyScrobbling, _rymUi.pageAlbum, currTime - song.duration);
postdata["s"] = sessID;
httpPost(submitURL, encodeParams(postdata), acceptSubmitResponseSingle, handleNetworkError);
}
function npNextTrack() {
const postdata = {};
postdata["a"] = toScrobble[currentlyScrobbling].artist;
postdata["t"] = toScrobble[currentlyScrobbling].trackName;
postdata["b"] = _rymUi.pageAlbum;
postdata["n"] = `${currentlyScrobbling + 1}`;
postdata["l"] = `${toScrobble[currentlyScrobbling].duration}`;
postdata["m"] = "";
postdata["s"] = sessID;
currTrackDuration = toScrobble[currentlyScrobbling].duration;
currTrackPlayTime = 0;
_scRYMbleUi.setMarquee(toScrobble[currentlyScrobbling].trackName);
httpPost(npURL, encodeParams(postdata), acceptNPResponse, handleNetworkError);
}
function timertick() {
let again = true;
if (currentlyScrobbling !== -1) {
if (currTrackDuration !== 0) {
_scRYMbleUi.setProgressBar(100 * currTrackPlayTime / currTrackDuration);
}
currTrackPlayTime++;
if (currTrackPlayTime === currTrackDuration) {
submitThisTrack();
again = false;
}
}
if (again && currentlyScrobbling !== -1) {
window.setTimeout(timertick, 1000);
}
}
function acceptHandshakeSingle(responseDetails) {
acceptHandshake(responseDetails, false);
}
function acceptHandshakeBatch(responseDetails) {
acceptHandshake(responseDetails, true);
}
function acceptHandshake(responseDetails, isBatch) {
if (responseDetails.status !== 200 || !responseDetails.isOkStatus) {
alertHandshakeFailed(responseDetails);
resetScrobbler();
return;
}
sessID = responseDetails.sessionId;
npURL = responseDetails.nowPlayingUrl;
submitURL = responseDetails.submitUrl;
if (isBatch) {
submitTracksBatch();
}
else {
npNextTrack();
}
}
function alertHandshakeFailed(responseDetails) {
if (responseDetails.responseText.indexOf("BADTIME") !== -1) {
alert("Handshake failed: Last.fm rejected this computer's time, even after scRYMble corrected for the difference it reported.\n\nPlease fix your system clock (check date, time, and time zone) and try again.");
return;
}
alert(`Handshake failed: ${responseDetails.status} ${responseDetails.statusText}\n\nData:\n${responseDetails.responseText}`);
}
function handleNetworkError(responseDetails) {
alert(`Network request failed: ${responseDetails.status} ${responseDetails.statusText}\n\nCheck your internet connection and try again.\n\nData:\n${responseDetails.responseText}`);
resetScrobbler();
}
function handshakeBatch() {
_scRYMbleUi.elementsOff();
handshake(_scRYMbleUi, acceptHandshakeBatch, handleNetworkError);
}
function scrobbleTest() {
console.log(_rymUi.pageAlbum);
toScrobble = buildListOfSongsToScrobble(_rymUi, _scRYMbleUi);
toScrobble.forEach((song, i) => {
const minutes = Math.floor(song.duration / 60);
const seconds = song.duration % 60;
const secondsStr = `00${seconds}`.slice(-2);
console.log(`${i + 1}. ${song.artist} — ${song.trackName} (${minutes}:${secondsStr})`);
});
}
(function () {
if (!_scRYMbleUi.isEnabled) {
return;
}
_scRYMbleUi.hookUpScrobbleNow(startScrobble);
_scRYMbleUi.hookUpScrobbleThen(handshakeBatch);
_scRYMbleUi.hookUpScrobbleTest(scrobbleTest);
window.addEventListener("beforeunload", confirmBrowseAway, true);
})();