Greasy Fork

Greasy Fork is available in English.

scRYMble

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         scRYMble
// @license      MIT
// @version      2.20240813021512
// @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 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;
    }
}

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 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.tracklistSongClass = ".song ";
        this.tracklistArtistClass = ".artist";
        //#endregion
    }
    get pageArtist() {
        var _a;
        return (_a = this.multipleByArtists) !== null && _a !== void 0 ? _a : this.singleByArtist;
    }
    get pageAlbum() {
        var _a;
        return (_a = document.querySelector(this.albumTitleClass).innerText.trim()) !== null && _a !== void 0 ? _a : "";
    }
    get multipleByArtists() {
        return Array.from(document.getElementsByClassName(this.creditedNameClass))
            .map(x => x)
            .map(x => x.innerText)[1];
    }
    get singleByArtist() {
        return Array.from(document.getElementsByTagName("span"))
            .filter(x => !!x.hasAttribute("itemprop") && x.getAttribute("itemprop") === this.byArtistProperty)[0].innerText;
    }
    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(this.tracklistSongClass);
        const lastSongTag = songTags[songTags.length - 1];
        const songTitle = (_a = lastSongTag === null || lastSongTag === void 0 ? void 0 : lastSongTag.innerHTML) !== null && _a !== void 0 ? _a : "";
        if (this.trackArtist(tracklistLine).length > 0 &&
            (songTitle.indexOf(" - ") === 0 || songTitle.indexOf("\n- ") === 0)) {
            // Artist-credited track list
            return songTitle.substring(3);
        }
        else {
            return songTitle;
        }
    }
    trackArtist(tracklistLine) {
        const artistTags = tracklistLine === null || tracklistLine === void 0 ? void 0 : tracklistLine.querySelectorAll(this.tracklistArtistClass);
        if (artistTags.length === 0)
            return "";
        if (artistTags.length === 1) {
            return artistTags[0].innerText;
        }
        // Multiple artists
        const entireSpan = tracklistLine.querySelector(this.tracklistTitleClass);
        const entireText = entireSpan.innerText;
        const dashIndex = entireText.indexOf("\n- ");
        return entireText.substring(0, dashIndex).replace(/\n/g, " ");
    }
    trackDuration(tracklistLine) {
        return (tracklistLine === null || tracklistLine === void 0 ? void 0 : tracklistLine.querySelector(this.tracklistDurationClass)).innerText.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);
    }
}

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 isVariousArtists() {
    const artist = _rymUi.pageArtist;
    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) {
        _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 buildListOfSongsToScrobble() {
    toScrobble = [];
    Array.from(_scRYMbleUi.checkboxes).forEach(checkbox => {
        if (checkbox.checked) {
            const tracklistLine = _rymUi.tracklistLine(checkbox);
            let songTitle = _rymUi.trackName(tracklistLine);
            let artist = _rymUi.pageArtist;
            const length = _rymUi.trackDuration(tracklistLine);
            if (isVariousArtists()) {
                artist = _rymUi.trackArtist(tracklistLine);
                if (artist.length === 0) {
                    // no dash exists! must be a single artist with " / " in the name or v/a with unscrobbleable list
                    if (artist.indexOf("Various Artists") > -1) {
                        artist = _rymUi.pageAlbum;
                    }
                }
            }
            else {
                const trackArtist = _rymUi.trackArtist(tracklistLine);
                if (trackArtist.length > 0) {
                    artist = trackArtist;
                }
            }
            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);
        }
    });
}
function submitTracksBatch(sessID, submitURL) {
    buildListOfSongsToScrobble();
    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();
    buildListOfSongsToScrobble();
    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);
    buildListOfSongsToScrobble();
    toScrobble.forEach((song, i) => {
        const minutes = Math.round(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);
})();