// ==UserScript==
// @name dlsite购物车增强
// @namespace http://tampermonkey.net/
// @version 1.1
// @description 为 DLsite 购物车添加评分、销量、发售日、标签等信息
// @author 0moi
// @match https://www.dlsite.com/maniax/cart
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant GM_xmlhttpRequest
// @license MIT
// @downloadURL none
// ==/UserScript==
(function () {
'use strict';
const ajaxApi = 'https://www.dlsite.com/maniax/product/info/ajax?cdn_cache_min=1&product_id=';
const jsonApi = 'https://www.dlsite.com/maniax/api/=/product.json?workno=';
const workMap = new Map(); // id → DOM
const tagMap = new Map(); // id → [{ old, new }]
const formatter = new Intl.DateTimeFormat("zh-CN", {
year: "numeric",
month: "long",
day: "numeric",
hour: "numeric",
hour12: false
});
async function main() {
importShoelace();
collectCartWorks();
await loadJsonTagData();
const ajaxJson = await loadAjaxData();
injectInfo(ajaxJson);
}
/* -----------------------
Step 1. 解析购物车作品
-------------------------*/
function collectCartWorks() {
const works = document.querySelectorAll('#cart_wrapper > ul > li');
works.forEach(work => {
const id = work.getAttribute('data-workno');
if (id) workMap.set(id, work);
});
}
/* -----------------------
Step 2. 加载 JSON 标签数据
-------------------------*/
async function loadJsonTagData() {
const tasks = [...workMap.keys()].map(id => fetchJsonData(id));
await Promise.all(tasks);
}
function fetchJsonData(id) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: jsonApi + id,
responseType: 'json',
onload: res => {
const data = res.response?.[0];
if (!data) return resolve();
const original = data.genres || [];
const replaced = data.genres_replaced || [];
// 通过 genre id 匹配,避免顺序问题
const tags = original.map(orig => {
const rep = replaced.find(x => x.id === orig.id);
return {
old: orig.name,
new: rep?.name ?? orig.name
};
});
tagMap.set(id, tags);
resolve(tags);
},
onerror: err => reject(err)
});
});
}
/* -----------------------
Step 3. 加载 AJAX 评分+销量数据
-------------------------*/
function loadAjaxData() {
const idStr = [...workMap.keys()].join(',');
const url = ajaxApi + idStr;
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url,
responseType: 'json',
onload: res => resolve(res.response || {}),
onerror: err => reject(err)
});
});
}
/* -----------------------
Step 4. 注入到 DOM
-------------------------*/
function injectInfo(json) {
workMap.forEach((dom, id) => {
const info = json[id];
if (!info) return;
const content = dom.querySelector('.work_content');
if (!content) return;
const frag = document.createDocumentFragment();
/* ---- 发售日 与 销量/评分 ---- */
const registDiv = createInfoBlock(info);
frag.appendChild(registDiv);
/* ---- 标签 ---- */
const tags = tagMap.get(id);
if (tags?.length) {
const tagDiv = document.createElement('dd');
tags.forEach(tag => {
tagDiv.appendChild(buildTag(tag.old, tag.new));
});
frag.appendChild(tagDiv);
}
content.appendChild(frag);
});
}
function createInfoBlock(info) {
const dl_count = info.dl_count ?? 0;
const avg = info.rate_average_2dp;
const rate_count = info.rate_count;
const date = new Date(info.regist_date);
const dateStr = formatter.format(date).replace(":", " 时");
const days = Math.floor((Date.now() - date.getTime()) / 86400000);
const dd = document.createElement('dd');
const rateHtml = rate_count
? ` 评分:
${avg}
(${rate_count})