localStorage + GM storage + 多账号隔离 — JM Shelf 推荐脚本的模块库,通过 @require 被主脚本引用。
Skrip ini tidak untuk dipasang secara langsung. Ini adalah pustaka skrip lain untuk disertakan dengan direktif meta // @require https://update.sleazyfork.org/scripts/581102/1842601/JM%20Shelf%20-%20Storage.js
// ==UserScript==
// @name JM Shelf - Storage
// @namespace jmshelf-lib
// @version 1.0.0
// @author Kesdi
// @description localStorage + GM storage + 多账号隔离 — JM Shelf 推荐脚本的模块库,通过 @require 被主脚本引用。
// @license MIT
// ==/UserScript==
//
// 此文件是 GreasyFork 库(library),不直接安装。
// 请安装主脚本: JM Shelf 给杂鱼的个性化推荐
//
// ═══ [5] STORAGE ═══ (localStorage + GM storage hybrid, 多账号隔离)
// ============================================================
const PREFIX = 'jms_';
let _accountKey = ''; // 动态: jms_Kiaelen_
function setAccount(username) {
if (!username) { _accountKey = ''; return; }
const newKey = PREFIX + username + '_';
if (newKey === _accountKey) return;
// 迁移: 如果旧数据存在(无账号前缀)且新账号数据为空, 则迁移
if (_accountKey === '' && username) {
const oldKeys = Object.keys(localStorage).filter(k => k.startsWith(PREFIX) && !k.includes('_' + username + '_'));
const hasNewData = Object.keys(localStorage).some(k => k.startsWith(newKey));
if (!hasNewData && oldKeys.length > 1) {
LOG.info(`📦 迁移 ${oldKeys.length} 条数据到账号 ${username}`);
for (const ok of oldKeys) {
const shortKey = ok.substring(PREFIX.length);
if (shortKey.startsWith(username + '_')) continue; // 跳过已迁移的
const val = localStorage.getItem(ok);
if (val) localStorage.setItem(newKey + shortKey, val);
}
// 不删旧数据, 留作备份
}
}
_accountKey = newKey;
}
const Storage = {
get(key) {
try {
// 优先读账号隔离数据
if (_accountKey) {
const raw = localStorage.getItem(_accountKey + key);
if (raw !== null && raw !== undefined) return JSON.parse(raw);
}
// 回退到全局数据
const raw = localStorage.getItem(PREFIX + key);
return raw ? JSON.parse(raw) : null;
} catch (e) { return null; }
},
set(key, val) {
try {
const target = _accountKey || PREFIX;
localStorage.setItem(target + key, JSON.stringify(val));
} catch (e) {}
},
remove(key) {
try {
if (_accountKey) localStorage.removeItem(_accountKey + key);
localStorage.removeItem(PREFIX + key);
} catch (e) {}
},
};
// Atomic GM storage for small, critical state (avoids tab race conditions)
const GMStore = {
get(key, fallback) {
try { const v = GM_getValue(PREFIX + key); return v !== undefined ? v : fallback; } catch (e) { return fallback; }
},
set(key, val) {
try { GM_setValue(PREFIX + key, val); } catch (e) {}
},
};
// ============================================================
// STATE (loaded once at init)
// NOTE: Using plain functions instead of getters/setters to avoid
// infinite recursion bugs. Each function reads/writes directly.
// ============================================================
const State = {
// Profile
getProfile() { return Storage.get('profile') || { tags: {}, authors: {}, types: {}, lastUpdate: 0, version: 0 }; },
saveProfile(v) {
const old = this.getProfile();
v.lastUpdate = Date.now();
v.version = (old.version || 0) + 1;
Storage.set('profile', v);
},
// Favorites
getFavorites() { return Storage.get('favorites') || []; },
saveFavorites(v) {
const seen = new Set();
v = (v||[]).filter(f => { const sid = String(f.id); if (seen.has(sid)) return false; seen.add(sid); return true; });
Storage.set('favorites', v);
},
// Liked (❤️点击爱心, 独立于收藏)
getLikedAlbums() {
const liked = Storage.get('liked') || [];
// 补时间戳: 旧记录没有likedAt的, 按现在算
let changed = false;
const now = Date.now();
for (const f of liked) {
if (!f.likedAt) { f.likedAt = now; changed = true; }
}
if (changed) this.saveLikedAlbums(liked);
return liked;
},
saveLikedAlbums(v) {
const seen = new Set();
v = (v||[]).filter(f => { const sid = String(f.id); if (seen.has(sid)) return false; seen.add(sid); return true; });
Storage.set('liked', v);
},
addLikedAlbum(id) {
const liked = this.getLikedAlbums();
const sid = String(id);
if (!liked.find(f => String(f.id) === sid)) {
liked.push({ id: sid, likedAt: Date.now() });
this.saveLikedAlbums(liked);
}
},
// History
getHistory() { return Storage.get('history') || []; },
saveHistory(v) { Storage.set('history', v); },
// Album cache
getAlbumCache() { return Storage.get('album_cache') || {}; },
saveAlbumCache(v) { Storage.set('album_cache', v); },
// Candidate pool
getCandidates() { return Storage.get('candidates') || []; },
saveCandidates(v) { Storage.set('candidates', v); },
getPoolSizes() { return Storage.get('poolSizes', {}); },
savePoolSizes(v) { Storage.set('poolSizes', v); },
// Recommendations
getRecommendations() { const r = Storage.get('recommendations'); return Array.isArray(r) ? r : []; },
saveRecommendations(v) { Storage.set('recommendations', v); },
// Scan state
getScanState() { return GMStore.get('scan_state', null); },
saveScanState(v) { GMStore.set('scan_state', v); },
getLastFullScan() { return GMStore.get('last_full_scan', 0); },
saveLastFullScan(v) { GMStore.set('last_full_scan', v); },
getLastRecalc() { return GMStore.get('last_recalc', 0); },
saveLastRecalc(v) { GMStore.set('last_recalc', v); },
getUsername() { return GMStore.get('username', ''); },
saveUsername(v) { GMStore.set('username', v); },
getFavoritesUrl() { return GMStore.get('fav_url', ''); },
saveFavoritesUrl(v) { GMStore.set('fav_url', v); },
getHistoryUrl() { return GMStore.get('hist_url', ''); },
saveHistoryUrl(v) { GMStore.set('hist_url', v); },
// Seen tracking: 记录本次/上次推荐过的ID, 用于降权
getSeenAlbums() { return Storage.get('seen') || []; },
saveSeenAlbums(v) { Storage.set('seen', v.slice(-CONFIG.MAX_SEEN_TRACK)); },
addSeenAlbum(id) {
const seen = this.getSeenAlbums();
if (!seen.includes(id)) { seen.push(id); this.saveSeenAlbums(seen); }
},
// Viewed albums: 持久记录所有浏览过的本子 (不限于推荐, 用户浏览历史之外)
// 注意: 这是个大型数组, 上限5000
getViewedAlbums() {
const v = Storage.get('viewed') || [];
// 补时间戳: 旧记录是纯ID字符串
let changed = false; const now = Date.now();
for (let i = 0; i < v.length; i++) {
if (typeof v[i] === 'string') { v[i] = { id: v[i], viewedAt: now }; changed = true; }
else if (!v[i].viewedAt) { v[i].viewedAt = now; changed = true; }
}
if (changed) this.saveViewedAlbums(v);
return v;
},
saveViewedAlbums(v) { Storage.set('viewed', v.slice(-5000)); },
addViewedAlbum(id) {
const viewed = this.getViewedAlbums();
const sid = String(id);
if (!viewed.find(v => String(v.id || v) === sid)) {
viewed.push({ id: sid, viewedAt: Date.now() });
this.saveViewedAlbums(viewed);
// 异步fetch标题 (不阻塞, 不在cache中就补)
const cache = this.getAlbumCache();
if (!cache[sid] || !cache[sid].title) {
(async () => {
try {
const html = await fetcher.enqueue('https://18comic.vip/album/'+sid+'/', null, 1);
if (html) { const d = Parser.parseDetail(html); if (d.title) { cache[sid] = { ...(cache[sid]||{}), id:sid, title:d.title, tags:d.tags||[], authors:d.authors||[], typeTags:d.typeTags||[] }; this.saveAlbumCache(cache); } }
} catch(e) {}
})();
}
}
},
clearViewedAlbums() { Storage.set('viewed', []); },
getTagFreq() { return Storage.get('tagfreq') || {}; },
saveTagFreq(v) { Storage.set('tagfreq', v); },
};