// ==UserScript== // @name Show points on Amazon.co.jp wishlist // @version 25.5.1 // @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/* // @match https://www.amazon.co.jp/s* // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_listValues // @grant GM_xmlhttpRequest // @compatible firefox // @license Apache-2.0 // @downloadURL https://update.greasyfork.icu/scripts/37063/Show%20points%20on%20Amazoncojp%20wishlist.user.js // @updateURL https://update.greasyfork.icu/scripts/37063/Show%20points%20on%20Amazoncojp%20wishlist.meta.js // ==/UserScript== /********************************************************************************************************************** NOTICE:このアプリケーションは国立国会図書館サーチAPI( https://iss.ndl.go.jp/information/api/ )と、openBDAPI( https://openbd.jp/ )を利用しています **********************************************************************************************************************/ (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 PROCESSES = 3; const COMMERCIAL_PUBLISHERS = [ '集英社', '講談社', 'KADOKAWA', '小学館', '日経BP', '東京書籍', '学研プラス', '文藝春秋', 'SBクリエイティブ', 'インプレス', 'DeNA', 'スクウェア・エニックス', 'ダイヤモンド社', 'ドワンゴ', '一迅社', '技術評論社', '近代科学社', '幻冬舎', '秋田書店', '少年画報社', '新潮社', '双葉社', '早川書房', '竹書房', '筑摩書房', '朝日新聞出版', '東洋経済新報社', '徳間書店', '日本文芸社', '白泉社', '扶桑社', '芳文社', '翔泳社', ]; const taxIncluded = listPrice => Math.floor(listPrice * (1 + TAX)); const isNull = value => value === null; const isUndefined = value => value === undefined; const hasValue = value => !isNull(value) && !isUndefined(value); const rate = ((numerator, denominator) => denominator === 0 ? 0 : 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 < 0) { return { color: '#9B1D1E', bgColor: 'initial', }; } else 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; }, ndlPublisher(publisher) { return 'https://iss.ndl.go.jp/api/sru?operation=searchRetrieve&recordSchema=dcndl&recordPacking=xml&maximumRecords=1&mediatype=1&query=publisher=' + publisher; }, amazon(asin) { return 'https://www.amazon.co.jp/dp/' + asin; }, openbd(isbn) { return 'https://api.openbd.jp/v1/get?isbn=' + isbn; }, } const storage = { async save(key, data) { console.log('SAVE: ' + key); if (!hasValue(key) || !hasValue(data)) { return null; } GM_setValue(key, JSON.stringify(data)); console.log('SAVED: ' + key, data); }, load(key) { console.log('LOAD: ' + key); if (!hasValue(key)) { return null; } const data = GM_getValue(key); console.log('LOADED: ' + key, data); if (!hasValue(data)) { return null; } return JSON.parse(data); }, exists(key) { return hasValue(GM_getValue(key)); }, async delete(key) { console.log('DELETE: ' + key); GM_deleteValue(key); }, list() { return GM_listValues(); }, clean() { const keys = this.list(); const now = Date.now(); for (const key of keys) { if (key === 'SETTINGS' || key === 'PUBLISHERS') { continue; } const data = this.load(key); if (now - data.updatedAt > CACHE_LIFETIME) { this.delete(key); } } }, isCacheActive(asin) { if (!storage.exists(asin)) { return false; } else { return Date.now() - storage.load(asin)?.updatedAt <= RESCAN_INTERVAL; } }, } 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 isAgeVerification(dom) { const element = dom.querySelector('#black-curtain-warning'); return !isNull(element); }, async isKindleUnlimited(dom) { const element = dom.querySelector('#tmm-ku-upsell'); return !isNull(element); }, async isbns(dom) { let isbns = []; const elements = dom.querySelectorAll('#tmmSwatches 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 Array.from(new Set(isbns)); }, async isBought(dom) { const element = dom.querySelector('#booksInstantOrderUpdate_feature_div'); if (isNull(element)) { return false; } return /購入/.test(element.innerText); }, async isKdp(dom) { const elements = dom.querySelectorAll("#detailBullets_feature_div .a-list-item"); for (const element of elements) { if (/出版社/.test(element.innerText)) { const m = element.querySelector('span:nth-child(2)').innerText.match(/^[^;(]*/); if (isNull(m) && hasValue(m[0])) { return true; } const publisher = m[0].trim(); console.log('publisher:' + publisher); const findIndex = COMMERCIAL_PUBLISHERS.findIndex(item => new RegExp(item).test(publisher)); if (findIndex !== -1) { return false; } let publishers = storage.load('PUBLISHERS'); if (isNull(publishers)) { publishers = {}; } else if (!isUndefined(publishers[publisher])) { return !publishers[publisher]; } const res = await get(url.ndlPublisher(publisher)); const hasPublisher = await parser.hasPublisher(res.responseXML); publishers[publisher] = hasPublisher; await storage.save('PUBLISHERS', publishers); return !hasPublisher; } } return true; }, async asin(dom) { const element = dom.querySelector("#ASIN"); if (isNull(element)) { return null; } return element.value; }, async kindlePrice(dom) { let element = dom.querySelector("#kindle-price"); if (!isNull(element)) { return parseInt(element.innerText.match(/[0-9,]+/)[0].replace(/,/, '')); } element = dom.querySelector("span.extra-message.olp-link"); if (!isNull(element)) { return parseInt(element.innerText.match(/[0-9,]+/)[0].replace(/,/, '')); } return null; }, 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 hasPublisher(xml) { const element = xml.querySelector("numberOfRecords"); if (isNull(element)) { return null; } return element.innerHTML !== '0'; }, async campaigns(dom) { return []; const elements = dom.querySelectorAll('span > div.a-section.a-spacing-none > div'); let tmp = []; for (const element of elements) { const spanTags = element.getElementsByTagName('span'); tmp.push(spanTags[0].innerText); } return tmp; }, wishlist: { async itemTitle(dom) { const element = dom.querySelector('a[id^="itemName_"]'); if (isNull(element)) { return null; } return 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 }, async isKindleItem(dom) { return /Kindle版/.test(dom.innerText); }, async isItemProcessed(dom) { return dom.classList.contains('SPAW_PROCESSED'); }, }, search: { async isKindleItem(dom) { const elements = dom.querySelectorAll('a.a-text-bold'); if (elements.length !== 0) { for (const element of elements) { if (/^Kindle版/.test(element.innerHTML.trim())) { return true; } } } return false; }, async title(dom) { const title = dom.querySelector("h2 > a"); if (isNull(title)) { return null; } return title.innerText.trim(); }, async asin(dom) { return dom.getAttribute("data-asin"); }, async isBulkBuy(dom) { return /まとめ買い/.test(dom.innerText); } }, } const lowPriceBook = (async (isbns) => Promise.all(isbns.map(async (isbn) => { try { let price = await getOpenBdPrice(isbn); if (hasValue(price)) { return { isbn: isbn, price: price, }; } price = await getNdlPrice(url.ndl(isbn)); return { isbn: isbn, price: price, } } catch (e) { return { isbn: isbn, price: null, } } })).then((prices) => { return prices.reduce((a, b) => a.price < b.price ? a : b); }) ); const getNdlPrice = (async (isbn) => { const res = await get(url.ndl(isbn)); return await parser.price(res.responseXML); }); const getOpenBdPrice = (async (isbn) => { const res = await get(url.openbd(isbn)); const json = JSON.parse(res.responseText); try { return json[0]['onix']['ProductSupply']['SupplyDetail']['Price'][0]['PriceAmount']; } catch (e) { return null; } }); const itemPage = { async itemInfo(dom) { if (await parser.isAgeVerification(dom)) { throw new Error('年齢確認が必要です'); } if (!await parser.isKindlePage(dom)) { return null; } const asin = await parser.asin(dom); if (!hasValue(asin)) { throw new Error('ASINが見つかりません'); } 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, isKdp: kindleInfo.isKdp, isKindleUnlimited: kindleInfo.isKindleUnlimited, campaigns: kindleInfo.campaigns, updatedAt: Date.now(), }; }); }, async kindleInfo(dom, data) { return Promise.all([ data, parser.kindlePrice(dom), parser.pointReturn(dom), parser.isBought(dom), parser.isKdp(dom), parser.isKindleUnlimited(dom), parser.campaigns(dom) ]).then(([data, kindlePrice, pointReturn, isBought, isKdp, isKindleUnlimited, campaigns]) => { const info = { price: isNull(kindlePrice) ? data.kindlePrice : kindlePrice, point: pointReturn, isBought: isBought, isKdp: isKdp, isKindleUnlimited: isKindleUnlimited, campaigns: campaigns, }; console.log('KINDLE INFO: ', info) return info; }); }, async bookInfo(dom, data) { if (hasValue(data) && hasValue(data.paperPrice)) { 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, } }, clickPrompt(dom) { let prompt = dom.querySelector('#buyOneClick .a-expander-prompt'); if (!isNull(prompt)) { prompt.click(); } }, async addPaperPrice(dom, paperPrice, kindlePrice) { if (isNull(paperPrice) || paperPrice === 0) { return; } paperPrice = taxIncluded(paperPrice); const off = paperPrice - kindlePrice; const offRate = Math.round(rate(off, paperPrice)); let html = '