Scrape product details and open a new tab with the data
// ==UserScript==
// @name Dakidex Scraper
// @namespace http://dakidex.com
// @version 1.6
// @description Scrape product details and open a new tab with the data
// @author Dakidex
// @match http*://booth.pm/*/items/*
// @match http*://*.booth.pm/items/*
// @grant none
// @license MIT
// ==/UserScript==
(function () {
"use strict";
// Material mapping from Japanese names to material tags
const japaneseToMaterialTag = {
アクライクラ: "material:fules_aqualycra",
アクアライクラ: "material:fules_aqualycra",
アクアヴェール: "material:fules_aquaveil",
アクアプレミア: "material:fules_aquapremier",
ライクトロンリッチ: "material:aj_lyctron_rich",
ライクトロン: "material:aj_lyctron",
ツーウェイロイカ: "material:2way_roika",
白桃: "material:shiromoufu_hakutou",
桜餅: "material:shiromoufu_sakuramochi",
メイプルシロップ: "material:maple_syrup",
};
class ProductScraper {
/**
* Gets the material tag for a Japanese material name
* @param {string} japaneseMaterialName - The Japanese material name
* @returns {string|null} The corresponding material tag, or null if not mapped
*/
static getMaterialTag(japaneseMaterialName) {
const normalized = japaneseMaterialName.trim();
return japaneseToMaterialTag[normalized] || null;
}
/**
* Extracts material name from product description
* Finds the first known material that appears in the text
* @param {string} descriptionText - The product description text
* @returns {string|null} The material name or null if not found
*/
static extractMaterialFromDescription(descriptionText) {
let firstMaterial = null;
let earliestIndex = Infinity;
// Check each known material and find which appears first in the text
for (const materialName of Object.keys(japaneseToMaterialTag)) {
const index = descriptionText.indexOf(materialName);
if (index !== -1 && index < earliestIndex) {
earliestIndex = index;
firstMaterial = materialName;
}
}
return firstMaterial;
}
/**
* Extracts release date from the product page
* Looks for the published date in "商品公開日時:YYYY-MM-DD HH:MM" format
* @returns {string|null} ISO format date string or null if not found
*/
static extractReleaseDate() {
const dateElement = document.querySelector("#js-item-published-date");
if (!dateElement) {
return null;
}
const text = dateElement.textContent;
// Match pattern: 商品公開日時:YYYY-MM-DD HH:MM
const dateMatch = text.match(/(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2})/);
if (dateMatch) {
const [, year, month, day, hour, minute] = dateMatch;
// Create ISO format date string (using JST timezone)
const date = new Date(`${year}-${month}-${day}T${hour}:${minute}:00+09:00`);
return date.toISOString();
}
return null;
}
/**
* Extracts all material tags from the product description
* @returns {string[]} Array of material tags found
*/
static extractMaterialTags() {
const tags = [];
// Try multiple selectors for different site layouts
let descElement =
document.querySelector(".js-market-item-detail-description") ||
document.querySelector('[class*="description"]') ||
document.querySelector('[class*="detail"]') ||
document.querySelector(".product-description") ||
document.querySelector(".item-explanation");
if (!descElement) {
return tags;
}
const allText = descElement.textContent;
const materialName = this.extractMaterialFromDescription(allText);
if (materialName) {
const materialTag = this.getMaterialTag(materialName);
if (materialTag) {
tags.push(materialTag);
}
}
// Check for limited order period
if (allText.includes("受付期間")) {
tags.push("meta:limited_order_period");
}
return tags;
}
static async scrape() {
const title = document.querySelector(".summary h2")?.textContent?.trim();
const price = document.querySelector(".variation-price")?.textContent;
let productUrl = window.location.href;
const realUrl = document.querySelector(
'a[data-product-list="from market_show via market_item_detail to shop_index"]'
)?.href;
if (realUrl) {
productUrl = realUrl + "items/" + productUrl.split("/").pop();
}
// Parse product images
let imageUrls = [];
const imageElements = document.querySelectorAll(".market-item-detail-item-image-wrapper img");
imageElements.forEach((img) => {
const imageUrl = img.getAttribute("data-origin") ?? img.getAttribute("src");
if (imageUrl) {
imageUrls.push(imageUrl);
}
});
// Ensure imageUrls are unique
imageUrls = [...new Set(imageUrls)];
// Move first image to last if there are more than 1 image
if (imageUrls.length > 1) {
imageUrls.push(imageUrls.shift());
}
// Extract material tags
const materialTags = this.extractMaterialTags();
// Extract release date
const releaseDate = this.extractReleaseDate();
const data = {
name: title || "",
price: this.parsePrice(price || ""),
currency: "JPY",
urls: [productUrl],
tags: materialTags,
release_date: releaseDate,
variants: [
{
name: "Default",
imageUrls: imageUrls,
nsfw: true,
},
],
};
const encodedData = btoa(encodeURIComponent(JSON.stringify(data)));
window.open(`https://dakidex.com/create?data=${encodedData}`, "_blank");
}
static parsePrice(price) {
return Number(price.replace(/[^0-9]/g, ""));
}
}
const button = document.createElement("button");
button.textContent = "Add to Dakidex";
button.style.position = "fixed";
button.style.bottom = "10px";
button.style.right = "10px";
button.style.zIndex = 1000;
button.style.padding = "10px";
button.style.borderRadius = "6px";
button.style.fontWeight = "bold"; // Made font bold
button.style.backgroundColor = "rgba(112, 112, 112, 0.7)"; // 70% gray background
button.style.color = "white"; // White text
button.addEventListener("click", () => ProductScraper.scrape());
document.body.appendChild(button);
})();