Greasy Fork

Greasy Fork is available in English.

RYM: Track Ratings Average when Rating

Displays your track rating averages (simple and time-weighted) in the track rating UI on the release page.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name        RYM: Track Ratings Average when Rating
// @match       https://rateyourmusic.com/release/*
// @version     1.0
// @namespace   https://github.com/fauu
// @author      fau
// @description Displays your track rating averages (simple and time-weighted) in the track rating UI on the release page.
// @license     MIT
// @grant       GM.addStyle
// @run-at      document-end
// ==/UserScript==
"use strict";

/* !!! ADVANCED OPTIONAL EXTRA FEATURE: Scaled score !!!

  An extra scaled score can be displayed along with the averages.

  How it's calculated:
    1. The RYM ratings (0.5-5.0) are replaced with corresponding predefined values.
       This is in order to support non-linear rating scales (so that e.g. 3.5->4.0 could
       be defined to constitute a larger jump in terms of the score than 3.0->3.5).
    2. The simple and time-weighted averages for the replacements are calculated.
    3. These averages are normalized to a 100-point scale using a predefined value for the
       equivalent of 100 points.


  To ENABLE this feature, paste the entire following block into the browser developer
  console (Ctrl+Shift+K in Firefox, Ctrl+Shift+J in Chrome) in any rateyourmusic.com tab:

    localStorage.setItem("trawr_config", `
    {
      "scaledEnabled": true,
      "scaledNorm100": 2.9,
      "scaledMap": [
        [0.5, 0],
        [1,   0.6],
        [1.5, 1.1],
        [2,   1.5],
        [2.5, 1.75],
        [3,   2],
        [3.5, 2.4],
        [4,   2.7],
        [4.5, 2.9],
        [5,   3.3]
      ]
    }
    `)

  , replacing the example parameters with the desired ones. Then press Enter and refresh
  the page.

  Parameters:
    `scaledEnabled`: (true/false) Whether to display the scaled score.
    `scaledNorm100`: (number) The value of the average that will translate to the score of
                     100.
    `scaledMap`: (list of pairs of numbers) A transformation map with the original RYM rating
                 values on the left (must not be modified) and the corresponding replacement
                 values for the score calculation on the right.


  To revert the change and DISABLE this feature, issue this command in the developer console:

    localStorage.removeItem("trawr_config")
*/

const avgsDecimals = 2;
const scoresDecimals = 0;
const scopeName = "trawr";
const configKey = `${scopeName}_config`;
const cssPrefix = scopeName;
const avgContainerClass = `${cssPrefix}_avg-container`;
const avgLabelClass = `${cssPrefix}_avg-label`;
const avgValueClass = `${cssPrefix}_avg-value`;
const hiddenClass = `${cssPrefix}_hidden`;

const css = `
#track_rating_status:is(.saved, .saving) > .${avgContainerClass} {
  margin-left: 1.05em;
}

.${avgLabelClass} {
  color: var(--mono-7);
}

.${avgValueClass} {
  font-family: monospace;
}

.${hiddenClass} {
  display: none;
}
`.trim();

function main() {
  let trackLengths = [];
  for (let el of document.querySelectorAll("#tracks > .track > .tracklist_line")) {
    const durationEl = el.querySelector(".tracklist_duration");
    if (!durationEl) {
      trackLengths = [];
      break;
    }
    const secs = parseInt(durationEl.dataset.inseconds);
    if (secs > 0) {
      trackLengths.push(secs);
    }
  }

  const myTrackRatingsEl = document.getElementById("my_track_ratings");
  if (!myTrackRatingsEl) return;
  const trackRatingsEl = myTrackRatingsEl.querySelector("#track_ratings");
  const trackRatingEls = Array.from(trackRatingsEl.children);
  const numTracks = trackRatingEls.length;
  if (!numTracks) return;

  GM.addStyle(css);

  const config = loadConfig();

  if (trackLengths.length !== numTracks) {
    trackLengths = [];
  }

  let avgContainerEl, avgValueEl;
  const observer = new MutationObserver((muts) => {
    let [sum, count, scaledSum, weightedSum, scaledWeightedSum, sumWeights] = [0, 0, 0, 0, 0, 0];
    const calcWeighted = trackLengths.length > 0;
    trackRatingEls.forEach((el, i) => {
      const rating = parseFloat(el.querySelector(".rating_num").textContent);
      if (Number.isNaN(rating)) return;
      sum += rating;
      const scaledRating = config.scaledEnabled && config.scaledMap.get(rating);
      if (config.scaledEnabled) {
        scaledSum += scaledRating;
      }
      count++;
      if (calcWeighted) {
        const weight = trackLengths[i];
        weightedSum += rating * weight;
        if (config.scaledEnabled) {
          scaledWeightedSum += scaledRating * weight;
        }
        sumWeights += weight;
      }
    });
    const avg = sum / count;
    const scaledAvg = config.scaledEnabled
      ? normScaled(scaledSum / count, config.scaledNorm100)
      : null;
    let weightedAvg, scaledWeightedAvg;
    if (calcWeighted) {
      weightedAvg = weightedSum / sumWeights;
      scaledWeightedAvg =
        config.scaledEnabled && normScaled(scaledWeightedSum / sumWeights, config.scaledNorm100);
    }

    if (Number.isNaN(avg)) {
      if (avgContainerEl) avgContainerEl.classList.add(hiddenClass);
      return;
    }

    if (!avgContainerEl) {
      avgContainerEl = document.createElement("div");
      avgContainerEl.classList.add(avgContainerClass);
      const avgLabelEl = document.createElement("span");
      avgLabelEl.innerHTML = "Average:  ";
      avgLabelEl.classList.add(avgLabelClass);
      avgContainerEl.append(avgLabelEl);
      avgValueEl = document.createElement("span");
      avgValueEl.classList.add(avgValueClass);
      avgContainerEl.append(avgValueEl);
      const statusEl = myTrackRatingsEl.querySelector("#track_rating_status");
      statusEl.append(avgContainerEl);
    }

    const baseScaledPart = config.scaledEnabled ? `/${scaledAvg.toFixed(scoresDecimals)}` : "";
    const basePart = avg.toFixed(avgsDecimals) + baseScaledPart;

    const weightedScaledPart =
      weightedAvg && config.scaledEnabled ? `/${scaledWeightedAvg.toFixed(scoresDecimals)}` : "";
    const weightedPart = weightedAvg
      ? ` (${weightedAvg.toFixed(avgsDecimals)}${weightedScaledPart} weighted)`
      : "";

    avgValueEl.textContent = basePart + weightedPart;

    avgContainerEl.classList.remove(hiddenClass);
  });

  observer.observe(trackRatingsEl, { childList: true, subtree: true });
}

function loadConfig() {
  const config = JSON.parse(localStorage.getItem(configKey) || "{}");
  config.scaledEnabled ||= false;
  config.scaledMap ||= [];
  config.scaledMap = new Map(config.scaledMap);
  config.scaledNorm100 ||= null;
  return config;
}

function normScaled(x, norm) {
  return (x / norm) * 100;
}

main();