// ==UserScript== // @name Show points on Amazon.co.jp wishlist // @version 20.4.2 // @description Amazon.co.jpの欲しいものリストで、Kindleの商品にポイントを表示しようとします // @namespace https://greasyfork.org/ja/users/165645-agn5e3 // @author Nathurru // @match https://www.amazon.co.jp/*/wishlist/* // @match https://www.amazon.co.jp/wishlist/* // @match https://www.amazon.co.jp/*/dp/* // @match https://www.amazon.co.jp/dp/* // @match https://www.amazon.co.jp/*/gp/* // @match https://www.amazon.co.jp/gp/* // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_listValues // @grant GM_xmlhttpRequest // @compatible Chrome // @license Apache-2.0 // @downloadURL none // ==/UserScript== /********************************************************************************************************************** NOTICE:このアプリケーションは国立国会図書館サーチAPI( https://iss.ndl.go.jp/information/api/ )を利用しています **********************************************************************************************************************/ (function () { 'use strict'; const domParser = new DOMParser(); const CACHE_LIFETIME = 1209600000; const RESCAN_INTERVAL = 10800000; const AUTOMATIC_CLEAN_FACTOR = 100; const TAX = 0.1; const taxIncluded = listPrice => Math.floor(listPrice * (1 + TAX)); const isNull = value => value === null; const isUndefined = value => value === undefined; const rate = ((numerator, denominator) => denominator === 0 ? 0 : Math.floor(numerator / denominator * 100)); const random = max => Math.floor(Math.random() * Math.floor(max)); const sleep = ms => new Promise((resolve) => setTimeout(resolve, ms)); const rateColor = rate => { if (rate < 20) { return { color: 'initial', bgColor: 'initial', }; } else if (rate < 50) { return { color: 'initial', bgColor: '#F7D44A', }; } else if (rate < 80) { return { color: '#FFFFFF', bgColor: '#FE7E03', }; } else { return { color: '#FFFFFF', bgColor: '#9B1D1E', }; } }; const url = { ndl(isbn) { return 'https://iss.ndl.go.jp/api/sru?operation=searchRetrieve&recordSchema=dcndl&recordPacking=xml&query=isbn=' + isbn; }, amazon(asin) { return 'https://www.amazon.co.jp/dp/' + asin; }, } const storage = { async save(key, data) { console.log('SAVE: ' + key, data); GM_setValue(key, JSON.stringify(data)); }, load(key) { const data = GM_getValue(key); if (isUndefined(data)) { return null; } console.log('LOAD: ' + key, data); return JSON.parse(data); }, exists(key) { return !isUndefined(GM_getValue(key)); }, async delete(key) { console.log('DELETE: ' + key); GM_deleteValue(key); }, list() { return GM_listValues(); }, clean() { console.log('CLEANING'); const keys = this.list(); const now = Date.now(); for (const key of keys) { const data = this.load(key); if (now - data.updatedAt > CACHE_LIFETIME) { this.delete(key); } } } } const storageClean = (() => { if (random(AUTOMATIC_CLEAN_FACTOR) === 0) { storage.clean(); } }) const isIsbn = ((isbn) => { let c = 0; if (isbn.match(/^4[0-9]{8}[0-9X]?$/)) { for (let i = 0; i < 9; ++i) { c += (10 - i) * Number(isbn.charAt(i)); } c = (11 - c % 11) % 11; c = (c === 10) ? 'X' : String(c); return c === isbn.charAt(9); } else if (isbn.match(/^9784[0-9]{9}?$/)) { for (let i = 0; i < 12; ++i) { c += Number(isbn.charAt(i)) * ((i % 2) ? 3 : 1); } c = ((10 - c % 10) % 10); return String(c) === isbn.charAt(12); } else { return false; } }); const get = (async (URL) => { console.log('GET: ' + URL); return new Promise((resolve, reject) => { const xhr = window.GM_xmlhttpRequest; xhr({ onabort: reject, onerror: reject, onload: resolve, ontimeout: reject, method: 'GET', url: URL, withCredentials: true, }); }); }); const parser = { async isKindlePage(dom) { const element = dom.querySelector('#title'); if (isNull(element)) { return false; } return /kindle版/i.test(element.innerText) }, async isbns(dom) { let isbns = []; const elements = dom.querySelectorAll('li.swatchElement .a-button a'); for (const element of elements) { const href = element.getAttribute("href"); if (isNull(href)) { continue; } const m = href.match(/\/(4[0-9]{8}[0-9X])/); if (!isNull(m) && isIsbn(m[1])) { isbns.push(m[1]); } } return isbns; }, async isBought(dom) { const element = dom.querySelector('#ebooksInstantOrderUpdate_feature_div'); if (isNull(element)) { return false; } return /商品を注文/.test(element.innerText); }, async asin(dom) { const element = dom.querySelector("input[name='ASIN.0']"); if (isNull(element)) { return null; } return element.value; }, async kindlePrice(dom) { const element = dom.querySelector(".kindle-price"); if (isNull(element)) { return 0; } return parseInt(element.innerText.match(/[0-9,]+/)[0].replace(/,/, '')); }, async pointReturn(dom) { let point = 0; const elements = dom.querySelectorAll(".swatchElement"); if (elements.length !== 0) { for (const element of elements) { if (!/Kindle/.test(element.innerText)) { continue; } const m = element.innerText.match(/([0-9,]+)pt/); if (!isNull(m)) { point = parseInt(m[1].replace(/,/, '')); break; } } } else { const element = dom.querySelector(".loyalty-points"); if (isNull(element)) { point = 0; } else { point = parseInt(element.innerText.match(/[0-9,]+/)[0].replace(/,/, '')); } } return isNaN(point) ? 0 : point; }, async price(xml) { const element = xml.querySelector("price"); if (isNull(element)) { return null; } const price = parseInt(element.innerHTML .replace(/[0-9]/g, s => String.fromCharCode(s.charCodeAt(0) - 0xfee0)) .match(/[0-9]+/)[0]); return isNaN(price) ? null : price; }, async itemTitle(dom) { const element = dom.querySelector('a[id^="itemName_"]'); if (isNull(element)) { return null; } return element.innerText; }, async isItemProcessed(dom) { return dom.classList.contains('SPAW_PROCESSED'); }, async isKindleItem(dom) { const element = dom.querySelector('span[id^="item-byline-"]'); if (isNull(element)) { return false; } return /Kindle版/.test(element.innerText); }, async itemAsin(dom) { const element = dom.querySelector('.price-section'); if (isNull(element)) { return undefined; } const attribute = element.getAttribute('data-item-prime-info'); if (isNull(attribute)) { return undefined; } return JSON.parse(attribute).asin } } const lowPriceBook = (async (isbns) => Promise.all(isbns.map(async (isbn) => { const request = await get(url.ndl(isbn)); const data = { isbn: isbn, price: await parser.price(request.responseXML), } console.log(data); return data; })).then((prices) => { return prices.reduce((a, b) => a.price < b.price ? a : b); }) ); const itemPage = { async itemInfo(dom) { if (!parser.isKindlePage(dom)) { return null; } const asin = await parser.asin(dom); console.log('ASIN: ' + asin); if (isUndefined(asin)) { throw new Error('ASIN NOT FOUND'); } const data = storage.load(asin); return Promise.all([ this.bookInfo(dom, data), this.kindleInfo(dom, data), ]).then(([bookInfo, kindleInfo]) => { return { asin: asin, isbn: bookInfo.isbn, paperPrice: bookInfo.price, kindlePrice: kindleInfo.price, pointReturn: kindleInfo.point, isBought: kindleInfo.isBought, updatedAt: Date.now(), }; }); }, async kindleInfo(dom, data) { return Promise.all([ parser.kindlePrice(dom), parser.pointReturn(dom), parser.isBought(dom), ]).then(([kindlePrice, pointReturn, isBought]) => { const data = { price: kindlePrice, point: pointReturn, isBought: isBought, }; console.log('KINDLE INFO: ', data) return data; }) }, async bookInfo(dom, data) { if (!isNull(data)) { console.log('USE CACHE: ' + data.asin) return { isbn: data.isbn, price: data.paperPrice, } } const isbns = await parser.isbns(dom); console.log('ISBN: ', isbns); if (isbns.length === 0) { return { isbn: null, price: null, }; } const book = await lowPriceBook(isbns); console.log('LOW: ', book) return { isbn: book.isbn, price: book.price, } }, async addPaperPrice(dom, paperPrice, kindlePrice) { if (isNull(paperPrice) || paperPrice === 0 || !isNull(dom.querySelector('.print-list-price'))) { return; } paperPrice = taxIncluded(paperPrice); const off = paperPrice - kindlePrice; const offRate = rate(off, paperPrice); let html = '