scRYMble

Visit a release page on rateyourmusic.com and scrobble the songs you see!

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         scRYMble
// @license      MIT
// @version      2.20250109031512
// @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      https://update.greasyfork.org/scripts/130/10066/Portable%20MD5%20Function.js
// ==/UserScript==
'use strict';

class HttpResponse {
    constructor(raw) {
        this.status = raw.status;
        this.statusText = raw.statusText;
        this.responseText = raw.responseText;
        this.lines = raw.responseText.split("\n");
    }
    get isOkStatus() {
        return this.lines[0] === "OK";
    }
    get sessionId() {
        return this.lines[1];
    }
    get nowPlayingUrl() {
        return this.lines[2];
    }
    get submitUrl() {
        return this.lines[3];
    }
}

function httpGet(url, onload) {
    GM_xmlhttpRequest({
        method: "GET",
        url,
        headers: {
            "User-agent": "Mozilla/4.0 (compatible) Greasemonkey"
        },
        onload: (responseRaw) => onload(new HttpResponse(responseRaw))
    });
}
function httpPost(url, data, onload) {
    GM_xmlhttpRequest({
        method: "POST",
        url,
        data,
        headers: {
            "User-agent": "Mozilla/4.0 (compatible) Greasemonkey",
            "Content-type": "application/x-www-form-urlencoded"
        },
        onload: (responseRaw) => onload(new HttpResponse(responseRaw))
    });
}

function fetch_unix_timestamp() {
    return parseInt(new Date().getTime().toString().substring(0, 10));
}
function stripAndClean(input) {
    input = input
        .replace("&", "")
        .replace("\n", " ");
    while (input.indexOf("  ") >= 0) {
        input = input.replace("  ", " ");
    }
    while (input.startsWith(" - ")) {
        input = input.substring(3);
    }
    while (input.startsWith("- ")) {
        input = input.substring(2);
    }
    return input.trim();
}

function handshake(ui, callback) {
    const username = ui.username;
    const password = ui.password;
    GM_setValue("user", username);
    GM_setValue("pass", password);
    const timestamp = fetch_unix_timestamp();
    const auth = hex_md5(`${hex_md5(password)}${timestamp}`);
    const handshakeURL = `http://post.audioscrobbler.com/?hs=true&p=1.2&c=scr&v=1.0&u=${username}&t=${timestamp}&a=${auth}`;
    httpGet(handshakeURL, callback);
}

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">&nbsp;
      <label for="${this.selectAllOrNoneId}" style="font-size: 60%;">select&nbsp;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}">&nbsp;</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}">&nbsp;</div>
          </td>
        </tr>
      </table>
    </td>
    <td>user: <input type="text" size="16" id="${this.usernameId}" value="${GM_getValue("user", "")}" /><br />
        pass: <input type="password" size="16" id="${this.passwordId}" value="${GM_getValue("pass", "")}"></input><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.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) {
        if (state) {
            this.scrobbleNowButton.removeAttribute("disabled");
            this.usernameInput.removeAttribute("disabled");
            this.passwordInput.removeAttribute("disabled");
        }
        else {
            this.scrobbleNowButton.setAttribute("disabled", "disabled");
            this.usernameInput.setAttribute("disabled", "disabled");
            this.passwordInput.setAttribute("disabled", "disabled");
        }
        for (const checkbox of this.checkboxes) {
            if (state) {
                checkbox.removeAttribute("disabled");
            }
            else {
                checkbox.setAttribute("disabled", "disabled");
            }
        }
    }
    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();
        const colon = durastr.indexOf(":");
        if (colon !== -1) {
            const minutes = parseInt(durastr.substring(0, colon));
            const seconds = parseInt(durastr.substring(colon + 1));
            this.duration = minutes * 60 + seconds;
        }
        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.status === 200) {
        if (!responseDetails.isOkStatus) {
            alertSubmitFailed(responseDetails);
        }
    }
    else {
        alertSubmitFailed(responseDetails);
    }
    if (isBatch) {
        _scRYMbleUi.setMarquee("Scrobbled OK!");
    }
    else {
        scrobbleNextSong();
    }
}
function alertSubmitFailed(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.status === 200) {
        if (!responseDetails.isOkStatus) {
            alertSubmitFailed(responseDetails);
        }
    }
    else {
        alertSubmitFailed(responseDetails);
    }
}
function submitTracksBatch(sessID, submitURL) {
    toScrobble = buildListOfSongsToScrobble(_rymUi, _scRYMbleUi);
    if (toScrobble === null)
        return;
    let currTime = fetch_unix_timestamp();
    const hoursFudgeStr = prompt("How many hours ago did you listen to this?");
    if (hoursFudgeStr !== null) {
        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 = currTime * 1 - toScrobble[i].duration * 1;
            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++) {
            postdata[`a[${i}]`] = toScrobble[i].artist;
            postdata[`t[${i}]`] = toScrobble[i].trackName;
            postdata[`b[${i}]`] = album;
            postdata[`n[${i}]`] = `${i + 1}`;
            postdata[`l[${i}]`] = `${toScrobble[i].duration}`;
            postdata[`i[${i}]`] = `${toScrobble[i].time}`;
            postdata[`o[${i}]`] = "P";
            postdata[`r[${i}]`] = "";
            postdata[`m[${i}]`] = "";
        }
        postdata["s"] = sessID;
        const postdataStr = Object.entries(postdata)
            .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
            .join("&");
        httpPost(submitURL, postdataStr, acceptSubmitResponseBatch);
    }
}
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("&nbsp;");
    _scRYMbleUi.setProgressBar(0);
    toScrobble = [];
    _scRYMbleUi.elementsOn();
}
function scrobbleNextSong() {
    currentlyScrobbling++;
    if (currentlyScrobbling === toScrobble.length) {
        resetScrobbler();
    }
    else {
        window.setTimeout(timertick, 10);
        handshake(_scRYMbleUi, acceptHandshakeSingle);
    }
}
function submitThisTrack() {
    const postdata = {};
    const i = 0;
    const currTime = fetch_unix_timestamp();
    postdata[`a[${i}]`] = toScrobble[currentlyScrobbling].artist;
    postdata[`t[${i}]`] = toScrobble[currentlyScrobbling].trackName;
    postdata[`b[${i}]`] = _rymUi.pageAlbum;
    postdata[`n[${i}]`] = `${currentlyScrobbling + 1}`;
    postdata[`l[${i}]`] = `${toScrobble[currentlyScrobbling].duration}`;
    postdata[`i[${i}]`] = `${currTime - toScrobble[currentlyScrobbling].duration}`;
    postdata[`o[${i}]`] = "P";
    postdata[`r[${i}]`] = "";
    postdata[`m[${i}]`] = "";
    postdata["s"] = sessID;
    const postdataStr = Object.entries(postdata)
        .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
        .join("&");
    httpPost(submitURL, postdataStr, acceptSubmitResponseSingle);
}
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);
    const postdataStr = Object.entries(postdata)
        .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
        .join("&");
    httpPost(npURL, postdataStr, acceptNPResponse);
}
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) {
        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: ${responseDetails.status} ${responseDetails.statusText}\n\nData:\n${responseDetails.responseText}`);
}
function handshakeBatch() {
    handshake(_scRYMbleUi, acceptHandshakeBatch);
}
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);
})();