Greasy Fork

Greasy Fork is available in English.

scRYMble

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

当前为 2024-08-28 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         scRYMble
// @license      MIT
// @version      2.20240828223135
// @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.icu/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(overridePageArtist) {
        this.overridePageArtist = overridePageArtist;
        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";
    }
    get isVariousArtists() {
        const artist = this.pageArtist;
        return artist.indexOf("Various Artists") > -1 ||
            artist.indexOf(" / ") > -1;
    }
    get pageArtist() {
        var _a, _b, _c;
        if (((_a = this.overridePageArtist) !== null && _a !== void 0 ? _a : "").length > 0)
            return (_b = this.overridePageArtist) !== null && _b !== void 0 ? _b : "";
        return (_c = this.multipleByArtists) !== null && _c !== void 0 ? _c : 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() {
        var _a;
        // Not using innerText because it doesn't work with Jest tests.
        const byArtistSpans = Array.from(document.getElementsByTagName("span"))
            .filter(x => !!x.hasAttribute("itemprop") && x.getAttribute("itemprop") === this.byArtistProperty);
        return byArtistSpans.length === 1
            ? (_a = byArtistSpans[0].textContent) !== null && _a !== void 0 ? _a : ""
            : "";
    }
    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;
        const songTags = tracklistLine === null || tracklistLine === void 0 ? void 0 : tracklistLine.querySelectorAll("[itemprop=name]");
        const lastSongTag = songTags[songTags.length - 1];
        let 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();
        }
        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) {
            console.log(`Couldn't determine artist for track "${songTitle}".`);
            artist = pageArtist.indexOf("Various Artists") > -1
                ? rymUi.pageAlbum
                : pageArtist;
        }
    }
    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(undefined);
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);
})();