Nico Mylist Rate (2025 New Design)

ニコニコ動画検索結果にマイリスト率を表示(新デザイン対応・HTTPS対応・自動再計算)

// ==UserScript==
// @name         Nico Mylist Rate (2025 New Design)
// @namespace    https://example.com/
// @version      0.4.1
// @description  ニコニコ動画検索結果にマイリスト率を表示(新デザイン対応・HTTPS対応・自動再計算)
// @match        *://www.nicovideo.jp/*
// @match        *://nico.ms/*
// @grant        none
// @run-at       document-idle
// @license MIT
// ==/UserScript==

(function () {
  'use strict';

  /* ---------- 共通ユーティリティ ---------- */
  function parseNumJP(raw) {
    // カンマ除去 & 単位(万・億)対応
    raw = raw || '';
    const s = raw.replace(/,/g, '').trim();
    if (s.endsWith('億')) return parseFloat(s) * 1e8;
    if (s.endsWith('万')) return parseFloat(s) * 1e4;
    return parseFloat(s) || 0;
  }

  function colorByRate(rate) {
    if (rate >= 6) return '#FF0000';// 赤
    if (rate >= 4) return '#0000FF'; // 青
    if (rate > 2) return '#87CEFA'; // 薄い青
    return '#393F3F'; // グレー
  }

  function createBadge(rate) {
    const badge = document.createElement('span');
    badge.textContent = ' Mylist率 ' + rate.toFixed(1) + '%';
    badge.style.cssText =
      'margin-left:6px;font-size:12px;font-weight:bold;color:' +
      colorByRate(rate) + ';';
    return badge;
  }

  /* ---------- 1) 検索/タグ結果 (.item) ---------- */
  function handleSearchItems(root) {
    root.querySelectorAll('li.item:not([data-mylist-checked])').forEach(function (item) {

      var viewEl = item.querySelector('li.count.view   .value');
      var mylistEl = item.querySelector('li.count.mylist .value');
      var view = parseNumJP(viewEl ? viewEl.textContent : '');
      var mylis = parseNumJP(mylistEl ? mylistEl.textContent : '');

      if (view) {
        var badge = createBadge((mylis / view) * 100);

        var listEl = item.querySelector('.itemData ul.list');
        if (listEl) {
          listEl.appendChild(badge);
        }
      }
      item.dataset.mylistChecked = '1';
    });
  }

  /* ---------- 2) マイリスト一覧 (.NC-系カード) ---------- */
  function handleMylistCards(root) {
    root.querySelectorAll('.NC-MediaObject-bodySecondary:not([data-mylist-rate])')
      .forEach(function (card) {

        var viewEl = card.querySelector('.NC-VideoMetaCount_view');
        var mylistEl = card.querySelector('.NC-VideoMetaCount_mylist');
        var view = parseNumJP(viewEl ? viewEl.textContent : '');
        var mylis = parseNumJP(mylistEl ? mylistEl.textContent : '');

        if (view) {
          var badge = createBadge((mylis / view) * 100);

          var metaCount = card.querySelector('.NC-VideoMediaObject-metaCount');
          if (metaCount) {
            metaCount.appendChild(badge);
          }
        }
        card.dataset.mylistRate = '1';
      });
  }

  /* ---------- 3) 初回 & MutationObserver ---------- */
  function injectAll(target) {
    target = target || document;
    handleSearchItems(target);
    handleMylistCards(target);
  }

  var obs = new MutationObserver(function (muts) {
    muts.forEach(function (m) { injectAll(m.target); });
  });
  obs.observe(document.body, { childList: true, subtree: true });

  injectAll(); // 初回実行
})();