JM Shelf - Init

入口初始化 + 样式 + XHR/fetch网络拦截 — JM Shelf 推荐脚本的模块库,通过 @require 被主脚本引用。

Dieses Skript sollte nicht direkt installiert werden. Es handelt sich hier um eine Bibliothek für andere Skripte, welche über folgenden Befehl in den Metadaten eines Skriptes eingebunden wird // @require https://update.sleazyfork.org/scripts/581113/1842615/JM%20Shelf%20-%20Init.js

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

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

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

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

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

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         JM Shelf - Init
// @namespace    jmshelf-lib
// @version      1.0.0
// @author       Kesdi
// @description  入口初始化 + 样式 + XHR/fetch网络拦截 — JM Shelf 推荐脚本的模块库,通过 @require 被主脚本引用。
// @license      MIT
// ==/UserScript==
// 
// 此文件是 GreasyFork 库(library),不直接安装。
// 请安装主脚本: JM Shelf 给杂鱼的个性化推荐
//

// ═══ [15] INIT ═══
  // ============================================================

  // ═══ 网络拦截: 监控收藏/爱心 XHR ═══
  (function() {
    const origSend = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.send = function(body) {
      const url = this._url || '';
      if (url.includes('/ajax/favorite_album') || url.includes('/ajax/delete_favorite_album') ||
          url.includes('/ajax/love_album') || url.includes('/ajax/unlove_album')) {
        try {
          const formData = new URLSearchParams(body);
          const albumId = formData.get('album_id');
          if (albumId) {
            if (url.includes('delete_favorite_album')) {
              LOG.info(`📌 网络监测: 取消收藏 album ${albumId}`);
              const favs = State.getFavorites();
              const idx = favs.findIndex(f => String(f.id) === String(albumId));
              if (idx >= 0) { favs.splice(idx, 1); State.saveFavorites(favs); showNotification('📌 已取消收藏'); }
            } else if (url.includes('favorite_album')) {
              LOG.info(`📌 网络监测: 收藏 album ${albumId}`);
              const favs = State.getFavorites();
              if (!favs.find(f => String(f.id) === String(albumId))) {
                const albumData = State.getAlbumCache()[String(albumId)] || { id: albumId, title: '', tags: [], authors: [], typeTags: [] };
                favs.push({ ...albumData, id: String(albumId) });
                State.saveFavorites(favs);
                showNotification('📌 已收藏!(network)');
              }
              try { _insertTitleToTrie(_initTrie(), albumId, albumData.title || '', 'fav'); _titleTrie.rebuildAfterInsert(); _titleTrie.save(); } catch(e) {}
            } else if (url.includes('unlove_album')) {
              LOG.info(`❤ 网络监测: 取消喜欢 album ${albumId}`);
              const liked = State.getLikedAlbums();
              const idx = liked.findIndex(f => String(f.id) === String(albumId));
              if (idx >= 0) { liked.splice(idx, 1); State.saveLikedAlbums(liked); showNotification('💔 已取消喜欢'); }
            } else if (url.includes('love_album')) {
              LOG.info(`❤ 网络监测: 喜欢 album ${albumId}`);
              State.addLikedAlbum(albumId);
              showNotification('❤️ 已记录喜欢!');
            }
          }
        } catch(e) {}
      }
      return origSend.apply(this, arguments);
    };
    const origOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function(method, url) {
      this._url = url;
      return origOpen.apply(this, arguments);
    };
  })();

  // ═══ fetch拦截: 兜底监控 ═══
  (function() {
    const origFetch = window.fetch;
    window.fetch = function(url, options) {
      const urlStr = typeof url === 'string' ? url : (url.url || '');
      const method = (options && options.method) || 'GET';
      if (method === 'POST' && (urlStr.includes('/ajax/favorite_album') || urlStr.includes('/ajax/delete_favorite_album') ||
          urlStr.includes('/ajax/love_album') || urlStr.includes('/ajax/unlove_album'))) {
        const body = (options && options.body) ? new URLSearchParams(options.body) : new URLSearchParams();
        const albumId = body.get('album_id');
        if (albumId) {
          if (urlStr.includes('delete_favorite_album')) {
            const favs = State.getFavorites();
            const idx = favs.findIndex(f => String(f.id) === String(albumId));
            if (idx >= 0) { favs.splice(idx, 1); State.saveFavorites(favs); showNotification('📌 已取消收藏'); }
          } else if (urlStr.includes('favorite_album')) {
            const favs = State.getFavorites();
            if (!favs.find(f => String(f.id) === String(albumId))) {
              const albumData = State.getAlbumCache()[String(albumId)] || { id: albumId, title: '', tags: [], authors: [], typeTags: [] };
              favs.push({ ...albumData, id: String(albumId) });
              State.saveFavorites(favs);
              showNotification('📌 已收藏!(network)');
            }
            try { const ad = State.getAlbumCache()[String(albumId)]; if (ad && ad.title) { _insertTitleToTrie(_initTrie(), albumId, ad.title, 'fav'); _titleTrie.rebuildAfterInsert(); _titleTrie.save(); } } catch(e) {}
          } else if (urlStr.includes('unlove_album')) {
            const liked = State.getLikedAlbums();
            const idx = liked.findIndex(f => String(f.id) === String(albumId));
            if (idx >= 0) { liked.splice(idx, 1); State.saveLikedAlbums(liked); showNotification('💔 已取消喜欢'); }
          } else if (urlStr.includes('love_album')) {
            State.addLikedAlbum(albumId);
            showNotification('❤️ 已记录喜欢!');
          }
        }
      }
      return origFetch.apply(this, arguments);
    };
  })();

  // 全局click监听: 喜欢按钮
  let _currentAlbumData = null;
  document.addEventListener('click', (e) => {
    const btn = e.target.closest('[id^="love_likes_"], [id^="love_heart_"], [id^="favorite_album_"]');
    if (!btn) return;
    const btnId = btn.id || '';
    const isLike = btnId.startsWith('love_likes_') || btnId.startsWith('love_heart_');
    const isFav = btnId.startsWith('favorite_album_');
    const idMatch = btnId.match(/\d+$/);
    const albumId = idMatch ? idMatch[0] : null;
    if (!albumId) return;
    if (!_currentAlbumData) { _currentAlbumData = { id: albumId, title: '', tags: [], authors: [] }; }

    if (isLike) {
      LOG.info(`❤ 检测到喜欢: album ${albumId}`);
      const oldProfile = State.getProfile();
      const liked = State.getLikedAlbums();
      const likedEntry = liked.find(f => String(f.id) === String(albumId));
      const daysSinceLiked = likedEntry ? (Date.now() - (likedEntry.likedAt||0)) / 86400000 : 0;
      const likeDecay = 0.1 + 0.9 * Math.exp(-daysSinceLiked / 30);
      const effectiveWeight = CONFIG.LIKE_WEIGHT * likeDecay;
      const newProfile = ProfileManager.update({ ...oldProfile }, _currentAlbumData, effectiveWeight);
      State.saveProfile(newProfile);
      State.addLikedAlbum(albumId);
      if (ProfileManager.changeRatio(oldProfile, newProfile) > CONFIG.PROFILE_CHANGE_RECALC) {
        recalcRecommendations();
      }
      showNotification('❤️ 已记录喜欢!');
    }

    if (isFav) return; // 收藏由XHR网络监控处理
  });

  // ═══ STYLES ═══
  function injectStyles() {
    GM_addStyle(`
      #jms-panel {
        position: fixed; bottom: 20px; right: 20px; width: 380px;
        background: #1a1a2e; color: #e0e0e0; border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.5);
        z-index: 99999; font-size: 13px; overflow: hidden;
        display: none;
      }
      #jms-panel-header {
        background: #16213e; padding: 10px 16px; font-weight: bold; font-size: 14px;
        display: flex; justify-content: space-between; align-items: center;
      }
      #jms-panel-header span { opacity: 0.9; }
      #jms-panel-close-x {
        background: none; border: none; color: #888; cursor: pointer; font-size: 20px; padding: 0 6px; line-height: 1;
      }
      #jms-panel-close-x:hover { color: #fff; }
      #jms-panel-body { padding: 16px; max-height: 65vh; overflow-y: auto; }
      #jms-status { color: #0f9; margin-bottom: 8px; font-size: 12px; }
      #jms-progress-container { margin: 10px 0; }
      #jms-progress-bar { height: 6px; background: #333; border-radius: 3px; overflow: hidden; }
      #jms-progress-fill { height: 100%; background: linear-gradient(90deg, #0f9, #0cf); width: 0; transition: width 0.3s; }
      #jms-progress-text { font-size: 11px; color: #aaa; margin-top: 4px; }
      #jms-stats { font-size: 12px; color: #aaa; margin: 8px 0; line-height: 1.6; }
      #jms-actions { display: flex; gap: 8px; margin: 10px 0; }
      #jms-settings-details { margin-top: 12px; font-size: 12px; }
      #jms-settings-details summary { cursor: pointer; color: #aaa; }
      #jms-history-details { margin-top: 12px; font-size: 12px; }
      #jms-history-details summary { cursor: pointer; color: #aaa; }
      #jms-log-details { margin-top: 12px; font-size: 12px; }
      #jms-log-details summary { cursor: pointer; color: #aaa; }
      #jms-settings { padding: 8px 0; }
      #jms-float-btn {
        position: fixed; bottom: 30px; right: 30px; width: 48px; height: 48px;
        border-radius: 50%; display: flex; align-items: center; justify-content: center;
        cursor: pointer; box-shadow: 0 4px 16px rgba(0,0,0,0.4);
        z-index: 99998; transition: transform 0.2s; overflow: hidden;
        animation: jms-float-rgb 20s ease-in-out infinite;
      }
      @keyframes jms-float-rgb {
        0%   { background: #ff4d6a; }
        16%  { background: #ff8c42; }
        33%  { background: #ffd166; }
        50%  { background: #4ecdc4; }
        66%  { background: #6c5ce7; }
        83%  { background: #e056a0; }
        100% { background: #ff4d6a; }
      }
      #jms-float-btn:hover { transform: scale(1.1); }
      #jms-notification {
        position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
        background: #1a1a2e; color: #fff; padding: 12px 24px; border-radius: 8px;
        z-index: 100000; font-size: 14px; box-shadow: 0 4px 16px rgba(0,0,0,0.4);
        animation: jms-fadeIn 0.3s ease;
      }
      @keyframes jms-fadeIn {
        from { opacity: 0; transform: translateX(-50%) translateY(-10px); }
        to { opacity: 1; transform: translateX(-50%) translateY(0); }
      }
    `);
  }

  // ═══ 数据迁移 ═══
  function migrateData(from) {
    if (from < 1) {
      try {
        const p = State.getProfile();
        if (p && !p.live) { p.live = { tags: {}, authors: {}, types: {}, lastUpdate: 0 }; State.saveProfile(p); }
      } catch(e) {}
      try {
        const v = State.getViewedAlbums();
        if (v.length > 0 && typeof v[0] === 'number') State.saveViewedAlbums(v.map(String));
      } catch(e) {}
      try { GMStore.set('scan_state', null); } catch(e) {}
    }
  }

  // ═══ 页面类型检测 ═══
  function detectPageType() {
    const path = location.pathname;
    if (path === '/' || path === '') return 'homepage';
    if (/\/album\/\d+/.test(path)) return 'album';
    if (/\/photo\/\d+/.test(path)) return 'photo';
    if (/\/albums/.test(path)) return 'listing';
    if (/\/search\/photos/.test(path)) return 'listing';
    if (/\/user\//.test(path)) return 'user';
    if (/\/theme\//.test(path)) return 'theme';
    return 'other';
  }

  // ═══ INIT ═══
  async function init() {
    LOG._restore();
    LOG.info('──────────────');

    const savedTemp = Storage.get('temperature');
    if (savedTemp != null) CONFIG.TEMPERATURE = savedTemp;

    const storedVersion = GMStore.get('version', 0);
    if (storedVersion < CONFIG.VERSION) {
      LOG.info(`版本升级 ${storedVersion} → ${CONFIG.VERSION}, 保留现有数据`);
      GMStore.set('version', CONFIG.VERSION);
    }

    const storedDataVer = GMStore.get('dataVersion', 0);
    if (storedDataVer < CONFIG.DATA_VERSION) {
      LOG.info(`数据格式迁移 ${storedDataVer} → ${CONFIG.DATA_VERSION}`);
      migrateData(storedDataVer);
      GMStore.set('dataVersion', CONFIG.DATA_VERSION);
    }

    injectStyles();

    const pageType = detectPageType();
    LOG.info(`当前页面类型: ${pageType}`);

    if (!isLoggedIn()) {
      LOG.info('未登录,仅注入UI框架');
      createPanel();
      return;
    }

    let username = detectUsername();
    if (!username) username = State.getUsername();
    if (username) {
      const lastUser = GMStore.get('lastUser', '');
      if (username !== lastUser) {
        GMStore.set('lastUser', username);
        State.saveUsername(username);
        LOG.info(`检测到用户: ${username}${lastUser ? ' (切换自 ' + lastUser + ')' : ''}`);
      }
      setAccount(username);
    }

    createPanel();

    if (!document.getElementById('jms-float-btn')) {
      const fb = document.createElement('div');
      fb.id = 'jms-float-btn';
      fb.title = 'JM Shelf';
      fb.innerHTML = '<img src="https://i.postimg.cc/BQ8vkqZv/JM-メスガキ.png" style="width:36px;height:36px;border-radius:50%">';
      fb.addEventListener('click', () => {
        const p = document.getElementById('jms-panel') || createPanel();
        p.style.display = p.style.display === 'block' ? 'none' : 'block';
      });
      document.body.appendChild(fb);
    }

    switch (pageType) {
      case 'homepage': handleHomepage(); break;
      case 'album': handleAlbumDetail(); break;
      case 'photo': handlePhotoPage(); break;
      case 'listing': handleListingPage(); break;
      case 'user': handleUserPage(); break;
    }

    LOG.info(`📌 已收藏/喜欢${State.getFavorites().length}本, 推荐${State.getRecommendations().length}条, 已看${State.getViewedAlbums().length}`);
    LOG.info('初始化完成');
  }

  // ═══ 调试工具 ═══
  try {
    unsafeWindow.jmsDebug = {
      score(id) {
        const cache = State.getAlbumCache();
        const album = cache[String(id)];
        if (!album) return `Album ${id} not in cache`;
        const profile = ProfileManager.getEffective(State.getProfile()||{});
        const favs = State.getFavorites();
        const hist = State.getHistory();
        const s = Recommender.score(album, profile, favs, hist, ProfileManager.deriveWeights(profile.popularityFingerprint||0.7));
        const maxTagW = Math.max(...Object.values(profile.tags), 1);
        const tf = State.getTagFreq();
        const fv = Object.values(tf).filter(v=>v>0);
        const mf = Math.max(...fv,1);
        const detail = (album.tags||[]).map(t => {
          const nt = normalizeTag(t);
          const w = profile.tags[nt]||0;
          if(w<=0) return null;
          const gc = tf[nt]||0;
          let idf = gc>0 ? clamp(1.0+Math.log10(Math.max(mf/gc,1))*0.6,0.6,2.0) : 1.0;
          const raw = w/maxTagW;
          return `${nt}(w${w} idf${idf.toFixed(2)} raw${raw.toFixed(2)}→${(raw*idf).toFixed(3)})`;
        }).filter(Boolean);
        return { id, title:album.title, score:s.score.toFixed(4), detail };
      },
      top(n) {
        const recs = State.getRecommendations().slice(0,n||10);
        const cache = State.getAlbumCache();
        return recs.map((r,i)=>{const al=cache[r.id];return `${i+1}. [${r.id}] ${(r._wm||'def')} s${r.score.toFixed(2)} ${(al?.title||'').slice(0,30)}`});
      }
    };
    LOG.info('调试工具已就绪: jmsDebug.score(albumId) / jmsDebug.top(N)');
  } catch (e) {}

  // ═══ START ═══
  init().catch(e => LOG.error('初始化失败', e));