Greasy Fork

Greasy Fork is available in English.

Amazon.co.jp検索非表示

Amazon.co.jpの検索で任意の商品を非表示。コマンド「Kindle注文済みスキャン」で注文済みを非表示にもできます

当前为 2020-05-21 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Amazon.co.jp検索非表示
// @namespace    https://www.amazon.co.jp/b/
// @version      1.1.15
// @description  Amazon.co.jpの検索で任意の商品を非表示。コマンド「Kindle注文済みスキャン」で注文済みを非表示にもできます
// @include      /^https:\/\/www\.amazon\.co\.jp\/([^\/]*?\/|gp\/aw\/|)[sb][\?\/]/
// @include      https://www.amazon.co.jp/gp/search/*
// @include      https://www.amazon.co.jp/gp/browse.html?*
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// @run-at       document-end
// @author       nanashi <[email protected]>
// ==/UserScript==

(() => {
    "use strict";

    // 設定
    const MAX_CROWLS = 4;							// Kindle注文済みスキャン時の最大同時クロール数
    const CROWL_INTERVAL = 1000;					// クロールの最低間隔(ミリ秒)
    const CACHE_TIME = 100 * 24 * 60 * 60 * 1000;	// キャッシュ管理での削除基準(最終参照からミリ秒)
    const SESSION_CACHE_TIME = 1 * 60 * 1000;		// 再スキャン禁止時間(ミリ秒)。個別で設定される
    const POP_TIME = 2800;							// ポップアップの表示時間(ミリ秒)
    const KAKUSU_BUTTON_TEXT = "非表示設定";			// 非表示設定ボタンのテキスト

    // DOM変更の監視対象を取得。なければ終了
    // ※2019年3月以前はAjaxで動的に更新されていたため監視が必要だった
    // ※2020年4月になり再びAjaxになった
    let target = document.getElementById("search-main-wrapper"); // URLのパスが/sの場合
    if(!target) target = document.getElementById("mainResults"); // URLのパスが/bの場合
    if(!target) target = document.getElementById("search"); // 2019年3月頃からのバージョン
    if(!target) return;

    // スタイルシートを一括追加
    GM_addStyle(
        // ポップアップ用のスタイルシート(Amazon側の命名と被らないようにダサいローマ字命名)
        "#oshirase-outer{" +
        "  position: fixed;" +
        "  top: 16px;" +
        "  left: 16px;" +
        "  padding: 0;" +
        "  border: 0;" +
        "  z-index: 9999;" +
        "}" +
        ".oshirase-pop, .oshirase-pop-large{" +
        "  display: table;" +
        "  color: black;" +
        "  line-height: 1.5em;" +
        "  margin-top: 0.25em;" +
        "  padding: 0.3em;" +
        "  background-color: white;" +
        "  border: 4px solid #090;" +
        "  border-radius: 4px;" +
        "  box-shadow: 0px 0px 4px 2px rgba(0,128,0,0.3);" +
        "}" +
        ".oshirase-pop{" +
        "  font-size: 18px;" +
        "}" +
        ".oshirase-pop-large{" +
        "  font-size: 28px;" +
        "}" +

        // 非表示、次に非表示、KDP疑いのスタイルシート
        ".kakusu{" +
        "  display: none !important;"+
        "}" +
        ".kakusu-temp, kakusu-bought, kakusu-kdp{" +
        "  display:inline-block;" +
        "}" +
        ".kakusu-temp > div, .kakusu-bought > div, .kakusu-kdp > div{" +
        "  border: 1px solid #00f !important;" +
        "  opacity: 0.5;" +
        "}" +
        ".kakusu-temp::before, .kakusu-bought::before, .kakusu-kdp::before{" +
        "  position: absolute;" +
        "  right: 0;" +
        "  top: 0;" +
        "  padding: 0.25em;" +
        "}" +
        ".kakusu-temp::before{" +
        "  content: \"次回から非表示\";" +
        "  color: #fff;" +
        "  background-color: #000;" +
        "}" +
        ".kakusu-bought::before{" +
        "  content: \"注文済み\";" +
        "  color: #fff;" +
        "  background-color: #00f;" +
        "}" +
        ".kakusu-kdp::before{" +
        "  content: \"KDP疑い\";" +
        "  color: #fff;" +
        "  background-color: #00f;" +
        "}" +

        // スポンサープロダクト非表示用のスタイルシート
        ".kakusu-sp{display: none !important;}" +

        // コンフィグ設定用のスタイルシート
        "#config-kakusu-cache, #config-setting{" +
        "  font-size: 14px;" +
        "  line-height: 18px;" +
        "  margin-top: 0.25em;" +
        "  padding: 1em;" +
        "  background-color: #efe;" +
        "  border: 4px solid #090;" +
        "  border-radius: 4px;" +
        "  box-shadow: 0px 0px 4px 2px rgba(0,128,0,0.3);" +
        "}" +
        "#config-kakusu-cache-textarea1{" +
        "  width: 100%;" +
        "  height: 256px;" +
        "  display: block;" +
        "}" +
        "#config-kakusu-cache-textarea2{" +
        "  width: 100%;" +
        "  height: 192px;" +
        "  display: block;" +
        "}" +
        "#config-kakusu-cache button{" +
        "  margin: 0.25em 0;" +
        "  padding: 0.25em;" +
        "}" +
        "#config-setting label{" +
        "  display: inline;" +
        "  padding: 0 0 0 0.25em;" +
        "  font-weight: normal;" +
        "}" +

        // 2019年3月頃からのAmazon側のスタイルシートに少し手を加える
        "span[data-component-type$=\"-search-results\"] div[data-asin]," +
        "span[data-component-type$=\"-search-results\"] div[data-asin] > div.sg-col-inner > div{" +
        "  position: relative;" +
        "}" +

        // 検索上部広告の非表示
        // 2020年5月現在、「マスク」で検索すると新型コロナウイルスのお知らせが表示されるが、これは広告ではないので消さないよう考慮した
        // 2020年5月下旬、Amzon側の仕様が少し変更されたので追加する
        (!localStorage.getItem("ADVERTISEMENT_SPACE_VISIBLE")
         ? "div#search > div[class*=\"desktop\"] div.sg-col-inner:only-child > span.rush-component[data-component-type=\"s-top-slot\"] > div[data-uuid]:only-child:not(class) div[id][data-creative-type]," +
           "div#search > div[class*=\"desktop\"] div.sg-col-inner:only-child > span.rush-component[data-component-type=\"s-search-results\"] > div > div[data-asin=\"\"][data-index=\"0\"]:first-child div[id][data-creative-type]{" +
           "  display: none;" +
           "}"
         : "") +

        // FEATURED ADVISERの非表示
        (!localStorage.getItem("FEATURED_ADVISER_VISIBLE")
         ? "#search div > span.celwidget[cel_widget_id*=\"SHOPPING_ADVISER\"]," +
           "#search div > span.celwidget[cel_widget_id*=\"FEATURED_ASINS_LIST\"]{" +
           "  display: none;" +
           "}"
         : "") +

        // 1-Click購入ボタンの非表示
        // display:noneで消すと要素を詰める再描写が少し気になるのでvisibility:hiddenにした。
        // 旧UIの方(URLが/b?のとき出現)はvisibility:hiddenだと空白が不自然なのでdisplay:noneのままにすることに。
        // 誤爆防止のためセレクタを長めに指定。
        (!localStorage.getItem("ONECLICK_BUTTON_VISIBLE")
         ? "#search div[data-asin] span[class*=\"oneclick\"][class*=\"button\"]," +
           "#search div[data-asin] span[class*=\"oneclick\"][class*=\"button\"] + div[class*=\"micro\"]," +
           "#search div[data-asin] span[class*=\"oneclick\"][class*=\"button\"] + br + div[class*=\"micro\"]," +
           "#search div[data-asin] span[class*=\"preorder\"][class*=\"button\"]," +
           "#search div[data-asin] span[class*=\"preorder\"][class*=\"button\"] + div[class*=\"micro\"]," +
           "#search div[data-asin] span[class*=\"preorder\"][class*=\"button\"] + br + div[class*=\"micro\"]{" +
           "  visibility: hidden;" +
           "}" +
           "#search-results li[data-asin] > div.s-item-container > div.a-fixed-left-grid div.a-col-right:last-child > div.a-row:last-child > div.a-column.a-span7:first-child > div.a-row > span[class*=\"button\"]:only-child," +
           "#search-results li[data-asin] > div.s-item-container > div.a-fixed-left-grid div.a-col-right:last-child > div.a-row:last-child > div.a-column.a-span7:first-child > div.a-row > span[class*=\"small\"]:only-child{" +
           "  display: none;" +
           "}"
         : "")
    );

    // 確認モード用スタイルシート
    const visible_style =
          ".kakusu, .kakusu-sp{" +
          "  display:inline-block !important;" +
          "  opacity: 0.5 !important;" +
          "}" +
          ".kakusu > div{" +
          "  border: 1px solid #00f !important;" +
          "}" +
          "div#search > div[class*=\"desktop\"] div.sg-col-inner:only-child > span.rush-component[data-component-type=\"s-top-slot\"] > div[data-uuid]:only-child:not(class) div[id][data-creative-type]," +
          "div#search > div[class*=\"desktop\"] div.sg-col-inner:only-child > span.rush-component[data-component-type=\"s-search-results\"] > div > div[data-asin=\"\"][data-index=\"0\"]:first-child div[id][data-creative-type]{" +
          "  display: block !important;" +
          "  opacity: 0.5 !important;" +
           "}" +
          "#search div > span.celwidget[cel_widget_id*=\"SHOPPING_ADVISER\"]," +
          "#search div > span.celwidget[cel_widget_id*=\"FEATURED_ASINS_LIST\"]{" +
          "  display: inline !important;" +
          "  opacity: 0.5 !important;" +
          "}" +
          "#search div[data-asin] span[id][class*=\"oneclick\"][class*=\"button\"]," +
          "#search div[data-asin] span[id][class*=\"preorder\"][class*=\"button\"]{" +
          "  visibility: visible !important;" +
          "}" +
          "#search div[data-asin] span[class*=\"oneclick\"][class*=\"button\"] + div[class*=\"micro\"]," +
          "#search div[data-asin] span[class*=\"oneclick\"][class*=\"button\"] + br + div[class*=\"micro\"]," +
          "#search div[data-asin] span[class*=\"preorder\"][class*=\"button\"] + div[class*=\"micro\"]," +
          "#search div[data-asin] span[class*=\"preorder\"][class*=\"button\"] + br + div[class*=\"micro\"]{" +
          "  visibility: visible !important;" +
          "}" +
          "#search-results li[data-asin] > div.s-item-container > div.a-fixed-left-grid div.a-col-right:last-child > div.a-row:last-child > div.a-column.a-span7:first-child > div.a-row > span[class*=\"button\"]:only-child," +
          "#search-results li[data-asin] > div.s-item-container > div.a-fixed-left-grid div.a-col-right:last-child > div.a-row:last-child > div.a-column.a-span7:first-child > div.a-row > span[class*=\"small\"]:only-child{" +
          "  display: inline-block;" +
          "}";

    ////////////////////////////////
    // Promiseを使ったsleep関数。async関数内でawaitを付けてコールする
    const sleep = (msec) => new Promise(resolve => setTimeout(resolve, msec));

    ////////////////////////////////
    // 要素の削除
    const removeElement = (e) => {
        if(e && e.parentNode) e.parentNode.removeChild(e);
    };

    ////////////////////////////////
    // ページ内から検索結果のリストを取得
    const getLIST = () => document.querySelectorAll('li[data-asin], span[data-component-type$="-search-results"] > div > div[data-asin]');

    ////////////////////////////////
    // ASINと見られる文字列をサニタイズ(空白や改行があるかもしれないので念のため)
    const sanitizeASIN = (asin) => {
        const a = String(asin).match(/[0-9A-Za-z]{10}/);
        if(!a) return null;
        return a[0];
    };

    ////////////////////////////////
    // 非表示キャッシュ(localStorageを使用)
    const setHideCcahe = (asin) => localStorage.setItem("NGASIN" + asin, String(Date.now()));
    const getHideCcahe = (asin) => Number(localStorage.getItem("NGASIN" + asin));
    const removeHideCcahe = (asin) => localStorage.removeItem("NGASIN" + asin);
    const clearHideCcahe = () => {
        const rx = /^NGASIN[0-9A-Za-z]{10}$/;
        for(const key in localStorage){
            if(rx.test(key)){
                localStorage.removeItem(key);
            }
        }
    };
    const expirationLocalCache = () => {
        const deadline = Date.now() - CACHE_TIME;
        let cache_count = 0;
        let remove_count = 0;
        let remove_asins = [];
        const slice_begin = "NGASIN".length; // 接頭文字の長さ
        for(const key in localStorage){
            if(/^NGASIN[0-9A-Za-z]{10}$/.test(key)){
                cache_count++;
                if(Number(localStorage.getItem(key)) <= deadline){
                    localStorage.removeItem(key);
                    remove_count++;
                    remove_asins.push(key.slice(slice_begin));
                }
            }else if(/^NGASIN/.test(key)){ // ゴミの可能性があるので削除
                localStorage.removeItem(key);
            }
        }
        setRemoveUndoCache(remove_asins);
        return [cache_count, remove_count]; // キャッシュ数(削除前)と削除数を返す
    };

    ////////////////////////////////
    // スキャン済みキャッシュ(sessionStorageなのでブラウザのタブを閉じると消える。また他のタブと共有もされない)
    const setScannedCache = (asin) => sessionStorage.setItem("SCANNED" + asin, String(Date.now()));
    const getScannedCache = (asin) => Number(sessionStorage.getItem("SCANNED" + asin));
    const removeScannedCache = (asin) => sessionStorage.removeItem("SCANNED" + asin);
    const clearScannedCache = () => {
        for(const key in sessionStorage) {
            if(/^SCANNED[0-9A-Za-z]{10}$/.test(key)){
                sessionStorage.removeItem(key);
            }
        }
    };
    const expirationSessionCache = () => {
        const deadline = Date.now() - SESSION_CACHE_TIME;
        for(const key in sessionStorage){
            if(/^SCANNED[0-9A-Za-z]{10}$/.test(key) && Number(sessionStorage.getItem(key)) <= deadline){
                sessionStorage.removeItem(key);
            }
        }
    };

    ////////////////////////////////
    // Undoキャッシュ(sessionStorageなのでブラウザのタブを閉じると消える。また他のタブと共有もされない)
    const clearUndoCache = () => {
        sessionStorage.removeItem("HIDE_UNDO_CACHE");
        sessionStorage.removeItem("REMOVE_UNDO_CACHE");
    };
    const setHideUndoCache = (asins) => {
        clearUndoCache();
        sessionStorage.setItem("HIDE_UNDO_CACHE", asins.join(","));
    };
    const getHideUndoCache = () => {
        const s = sessionStorage.getItem("HIDE_UNDO_CACHE");
        if(!s) return [];
        return s.split(",");
    };
    const setRemoveUndoCache = (asins) => {
        clearUndoCache();
        sessionStorage.setItem("REMOVE_UNDO_CACHE", asins.join(","));
    };
    const getRemoveUndoCache = () => {
        const s = sessionStorage.getItem("REMOVE_UNDO_CACHE");
        if(!s) return [];
        return s.split(",");
    };

    ////////////////////////////////
    // ポップアップなどを表示する場所(#oshirase-outer)のエレメント取得。なければ作成もする
    const getElementOshiraseOuter = () => {
        let outer = document.getElementById("oshirase-outer");
        // outer部分がなければ作成
        if(!outer){
            outer = document.createElement("div");
            outer.setAttribute("id", "oshirase-outer");
            document.getElementsByTagName("body")[0].appendChild(outer);
        }
        return outer;
    };

    ////////////////////////////////
    // ポップアップ用の要素作成
    const createPopupElement = (class_name, default_text) => {
        const e = document.createElement("div");
        if(class_name) e.classList.add(class_name);
        if(default_text) e.textContent = default_text;
        const outer = getElementOshiraseOuter();
        outer.appendChild(e);
        return e;
    };

    ////////////////////////////////
    // 要素をフェードアウトさせながら削除
    const fadeoutElement = (e) => {
        if(!e) return;
        setTimeout(async ()=>{
            for(let i = 15; i > 0; --i){
                e.style.opacity = (i/16.0).toFixed(5);
                e.style.transform = "scaleY(" + (i/16.0).toFixed(5) + ")";
                await sleep(10);
            }
            removeElement(e);
        }, POP_TIME);
    };

    ////////////////////////////////
    // 上のcreatePopupElementとfadeoutElementで簡易ポップアップ表示
    const popup = (text, large) => fadeoutElement(createPopupElement(large?"oshirase-pop-large":"oshirase-pop", text));

    ////////////////////////////////
    // キャッシュデータに基づいてNG ASINを非表示(MutationObserver対応)
    const hideNGASIN = (records) => {
        let li_list = [];
        // 引数recordsが有効ならMutationObserverで呼び出された
        if(records){
            for(const record of records){
                for(const node of record.addedNodes){
                    if(node.getAttribute("data-asin")){
                        li_list.push(node)
                    }else if(node.querySelectorAll){
                        li_list = li_list.concat(Array.from(node.querySelectorAll('li[data-asin],data[data-asin]')));
                    }
                }
            }
        }else{
            li_list = getLIST(); // ページ内からリストを取得
        }

        // 0個なら終了
        if(li_list.length == 0){
            return;
        }

        const b_sponcer_product_hide = !localStorage.getItem("SPONSOR_PRODUCT_VISIBLE");// スポンサープロダクト非表示フラグ
        const b_kakusu_button_visible = !localStorage.getItem("KAKUSU_BUTTON_HIDE");// 非表示設定ボタン表示フラグ
        const b_price_emphasis = !localStorage.getItem("PRICE_EMPHASIS_OFF");// 価格強調フラグ

        // 「非表示設定」の部分。これのクローンを作って使う
        let anode = null;
        if(b_kakusu_button_visible){
            anode = document.createElement("a");
            anode.classList.add("a-size-small");
            anode.setAttribute("href", "javascript:void(0);");
            anode.setAttribute("kakusu", "config");
            anode.textContent = KAKUSU_BUTTON_TEXT;
            anode.style = "position:absolute;right:0;bottom:0;";
        }

        // メインループ
        let hide_count = 0;
        let sp_count = 0;
        for(const li of li_list) {
            const asin = sanitizeASIN(li.getAttribute("data-asin"));
            if(!asin) continue;
            if(getHideCcahe(asin)){
                setHideCcahe(asin);
                li.classList.add("kakusu");
                hide_count++;
            }else if(b_sponcer_product_hide && li.querySelector('h5[class*="sponsored"],div[data-component-type*="sponsored"]')){
                // スポンサープロダクトの非表示
                li.classList.add("kakusu-sp");
                hide_count++;
                sp_count++;
            }else{
                li.classList.remove("kakusu");
                li.classList.remove("kakusu-sp");
            }
            li.classList.remove("kakusu-temp");
            li.classList.remove("kakusu-bought");
            li.classList.remove("kakusu-kdp");

            // 「非表示設定」を追加する
            if(b_kakusu_button_visible){
                const tnode = li.querySelector('div.s-item-container > div:last-child, div.sg-col-inner > div.s-include-content-margin, div.sg-col-inner div.a-section');
                if(tnode && !tnode.querySelector('a[kakusu="config"]')){
                    tnode.appendChild(anode.cloneNode(true));
                    // cloneNodeではイベント処理まではコピーされないのでここでクリック時のイベント処理を追加するつもりだった。
                    // しかし、Amazon側でもcloneNodeを使っており、イベント処理が消えてしまう場合があるのを確認。
                    // 別のやり方でクリック時のイベント処理することにした。
                }
            }

            // 目立たない文字色にされている価格やポイントを分かりやすく強調する
            // (unlimited対象の通常価格が目立たなくされている)
            if(b_price_emphasis){
                for(const span of li.querySelectorAll('div.a-row > span')){
                    let m = span.innerHTML.match(/^(\s*または.*?)(¥[0-9][0-9,]*)(\s*で購入.*)$/);
                    if(m){
                        span.innerHTML = m[1] + "<span style=\"color:#B12704!important;font-size:120%!important;\">" + m[2] + "</span>" + m[3];
                        continue;
                    }
                    m = span.innerHTML.match(/^(\s*Amazon\s*ポイント\s*[::]\s*)([1-9][0-9,]*\s*pt\s*\([0-9,\.]+[%%]\))(\s*)$/i);
                    if(m){
                        span.innerHTML = m[1] + "<span style=\"color:#B12704!important;\">" + m[2] + "</span>" + m[3];
                        continue;
                    }
                }
            }
        }

        // 一つでも非表示があればポップアップを表示
        if(hide_count > 0){
            let result_str = hide_count + "件非表示";
            if(sp_count > 0) result_str += "(スポンサープロダクト" + sp_count + "件)";
            popup(result_str);
        }
    };

    ////////////////////////////////
    // クリック時のイベント処理(「非表示設定」をクリックした際の処理)
    if(!localStorage.getItem("KAKUSU_BUTTON_HIDE")){ // 非表示設定ボタンが非表示なら不要な処理
        document.addEventListener("click", ((event) => {
            // 「非表示設定」をクリックしたか判定
            const e = event.target;
            if(e.getAttribute("kakusu") != "config") return true;

            // 親要素を巡っていき、最初に見つけたdata-asinのASINを使う
            let li = e.parentNode;
            let asin = null;
            while(li){
                asin = sanitizeASIN(li.getAttribute("data-asin"));
                if(asin) break;
                li = li.parentNode;
            }
            if(!asin) return true;

            // 表示/非表示を切り替える
            if(!getHideCcahe(asin)){
                setHideCcahe(asin); // キャッシュ更新(最終参照時刻を更新)
                li.classList.add("kakusu-temp"); // すぐ消さずに仮置き状態
                setHideUndoCache([asin]);
            } else {
                removeHideCcahe(asin);
                li.classList.remove("kakusu");
                li.classList.remove("kakusu-temp");
                li.classList.remove("kakusu-bought");
                li.classList.remove("kakusu-kdp");
                li.classList.remove("kakusu-sp");
                clearUndoCache();
            }
            event.preventDefault(); // ページ遷移無効
            return false;
        }), false);
    }

    ////////////////////////////////
    // 表示中の商品を全て非表示設定する
    const hideAll = () => {
        let hide_asins = [];
        for(const li of getLIST()) {
            const asin = sanitizeASIN(li.getAttribute("data-asin"));
            if(asin && !getHideCcahe(asin)){
                setHideCcahe(asin); // 非表示キャッシュに追加
                li.classList.add("kakusu-temp");
                hide_asins.push(asin);
            }
        }
        setHideUndoCache(hide_asins);
        popup(hide_asins.length + "件が次回から非表示"); // ポップ表示
    };

    ////////////////////////////////
    // スキャンして購入済みASINをキャッシュに保存
    const scanBought = async () => {
        // 検索結果のリストを取得
        const li_list = getLIST();
        if(li_list.length == 0){
            popup("検索結果がありません", true);
            return;
        }

        // スキャン済みキャッシュの中から期限切れのものを削除(sessionStorageなので消えてることが多い)
        expirationSessionCache();

        // KDP疑いを含めるか?のフラグ
        const b_perhaps_kdp_hide = !!localStorage.getItem("PERHAPSKDP_HIDE");

        // クロールリスト作成
        let crawl_asins = [];
        let skip_count = 0;
        for(const li of li_list) {
            const asin = sanitizeASIN(li.getAttribute("data-asin"));
            if(!asin) continue;
            if(getHideCcahe(asin)){
                setHideCcahe(asin); // キャッシュ更新(最終参照時刻を更新)
            }else if(getScannedCache(asin)){
                skip_count++;
            }else if(window.getComputedStyle(li).display == "none"){
                // すでに非表示(他のスクリプトで消された可能性)
            }else{
                crawl_asins.push(asin); // クロール対象
            }
        }

        const prog = createPopupElement("oshirase-pop-large", "-"); // 進捗をポップアップでお知らせする部分
        let bought_asins = [];
        let kdp_asins = [];
        let scanned_asins = [];
        let error_asins = [];
        let last_crawl_time = 0;
        while(1){
            // 残り0個になったら終了
            const remain = crawl_asins.length;
            if(remain <= 0) break;
            prog.textContent = crawl_asins[0] + "あたりをスキャン中(残り" + remain + ")";

            // 前回との間隔がCROWL_INTERVALより短い場合はスリープする
            let now_time = Date.now();
            const sleep_time = last_crawl_time + CROWL_INTERVAL - now_time;
            if(sleep_time > 0){
                await sleep(sleep_time);
                now_time = Date.now();
            }
            last_crawl_time = now_time;

            // crawl_asinsからASINをMAX_CROWLS個取り出しクロール
            let task_list = [];
            for(let i = 0; i < MAX_CROWLS && crawl_asins.length > 0; i++){
                const asin = crawl_asins.shift();
                task_list.push(
                    fetch("https://www.amazon.co.jp/dp/" + asin, {
                        credentials: "include",
                        referrerPolicy: "no-referrer"
                    }).then((response) => {
                        if(!response.ok) throw Error(response.statusText);
                        return response.text();
                    }).then((text) => {
                        const offset = text.indexOf('id="ebooksInstantOrderUpdate_feature_div"') + 1; // Kindle本のページか判定
                        if(offset > 0){
                            if(text.indexOf('id="ebooksInstantOrderUpdate"', offset) > offset){
                                setHideCcahe(asin); // 非表示キャッシュに追加
                                bought_asins.push(asin);
                            }else if(b_perhaps_kdp_hide && !(/[>\r\n]出版社:[<\r\n]/.test(text))){
                                setHideCcahe(asin); // 非表示キャッシュに追加
                                kdp_asins.push(asin);
                            }else{
                                setScannedCache(asin); // スキャン済みキャッシュに追加
                                scanned_asins.push(asin);
                            }
                        }else{ // Kindle本以外([まとめ買い]も含まれる。[まとめ買い]は新刊が追加される可能性があるので自動で非表示にしない)
                            setScannedCache(asin); // スキャン済みキャッシュに追加
                            scanned_asins.push(asin);
                        }
                    }).catch((error) => {
                        console.error(error);
                        error_asins.push(asin);
                    })
                )
            }
            // awaitで待つ
            await Promise.all(task_list);
        }
        let result_str = "スキャン終了<br>" + (bought_asins.length + kdp_asins.length) + "件をキャッシュに追加";
        if(b_perhaps_kdp_hide) result_str += "(うち" + kdp_asins.length + "件がKDP疑い)";
        if(skip_count > 0) result_str += "<br>" + skip_count + "件はスキャンしたばかりなのでスキップ";
        if(error_asins.length > 0) result_str += "<br>" + error_asins.length + "件がエラーになりました";
        setHideUndoCache(bought_asins.concat(kdp_asins, scanned_asins))
        prog.innerHTML = result_str;

        // 非表示処理をする
        for(const li of getLIST()){
            const asin = sanitizeASIN(li.getAttribute("data-asin"));
            if(!asin) continue;
            if(bought_asins.includes(asin)){
                li.classList.add("kakusu-bought");
            }else if(kdp_asins.includes(asin)){
                li.classList.add("kakusu-kdp");
            }
        }
        fadeoutElement(prog); // 進捗のポップアップをフェードアウトしながら削除
    };

    ////////////////////////////////
    // スキャンや全部非表示による非表示をUndo(取り消し)
    // NGASIN管理の「○日以上参照のないものを削除」のUndoも行う
    const undo = () => {
        let pop_text = "";
        const hide_undo_asins = getHideUndoCache();
        const remove_undo_asins = getRemoveUndoCache();
        if(hide_undo_asins.length <= 0 && remove_undo_asins.length <= 0){
            pop_text = "Undo情報なし";
        }else{
            // 非表示キャッシュを元に戻す
            for(const asin of hide_undo_asins){
                removeHideCcahe(asin);
                removeScannedCache(asin);
            }
            for(const asin of remove_undo_asins){
                setHideCcahe(asin);
            }
            // 非表示を取りやめ
            const li_list = getLIST();
            if(li_list.length > 0){
                for(const li of li_list) {
                    const asin = sanitizeASIN (li.getAttribute("data-asin"));
                    if(asin && hide_undo_asins.includes(asin)){
                        li.classList.remove("kakusu");
                        li.classList.remove("kakusu-temp");
                        li.classList.remove("kakusu-bought");
                        li.classList.remove("kakusu-kdp");
                    }
                }
            }
            pop_text = "Undo終了(";
            if(hide_undo_asins.length){
                pop_text += hide_undo_asins.length + "件の非表示をキャンセル";
            }
            if(hide_undo_asins.length > 0 && remove_undo_asins.length > 0){
                pop_text += ", ";
            }
            if(remove_undo_asins.length > 0){
                pop_text += remove_undo_asins.length + "件のNGASINを復帰";
            }
            pop_text += ")";
        }
        clearUndoCache();

        // ポップ表示
        popup(pop_text);
    };

    ////////////////////////////////
    // 非表示/確認。トグル動作
    const displayHide = () => {
        const e = document.getElementById("kakusu-visible");
        let pop_text = "";
        if(!e){
            // スタイルシートを追加してこちらを優先させる
            const head = document.getElementsByTagName("head")[0];
            if(!head) return;
            pop_text = "確認モード";
            const style = document.createElement("style");
            style.setAttribute("id", "kakusu-visible");
            style.setAttribute("type", "text/css");
            style.innerHTML = visible_style;
            head.appendChild(style);
        }else{
            // スタイルシートを削除してデフォルトに戻す
            removeElement(e);
            pop_text = "非表示モード";
            // kakusu-temp、kakusu-bought、kakusu-kdpなどの仮置きをkakusuにする
            const btemps = Array.from(document.getElementsByClassName("kakusu-temp"))
            .concat(Array.from(document.getElementsByClassName("kakusu-bought")))
            .concat(Array.from(document.getElementsByClassName("kakusu-kdp")));
            for(const btemp of btemps){
                btemp.classList.add("kakusu");
                btemp.classList.remove("kakusu-temp");
                btemp.classList.remove("kakusu-bought");
                btemp.classList.remove("kakusu-kdp");
            }
        }
        // ポップ表示
        popup(pop_text, true);
    };

    ////////////////////////////////
    // 追加設定(スポンサープロダクトやFEATURED ADVISERの表示設定、スキャン時のKDP扱いなど)
    const configSetting = () => {
        let config = document.getElementById("config-setting");
        if(config) return; // すでに表示されいてる

        // ダイアログ部分作成
        config = document.createElement("div");
        config.id = "config-setting";

        // キャプション
        const caption1 = document.createElement("span");
        caption1.innerHTML = "【追加設定】<br/> <br/>";

        // ON/OFFのチェックボックス作成(最近はinput要素をformで囲わなくても大丈夫らしい)
        const cahnge_chkbox = (id, keyname) => {
            if(document.getElementById(id).checked){
                localStorage.setItem(keyname, "1");
            }else{
                localStorage.removeItem(keyname);
            }
            popup("設定を変更しました");
        };
        const create_chkbox = (id, keyname, html) => {
            const div = document.createElement("div");
            const chkbox = document.createElement("input");
            chkbox.type = "checkbox";
            chkbox.id = id;
            chkbox.checked = !!localStorage.getItem(keyname);
            chkbox.addEventListener("change", () => cahnge_chkbox(id, keyname) );
            div.appendChild(chkbox);
            const chkbox_label = document.createElement("label");
            chkbox_label.htmlFor = id;
            chkbox_label.innerHTML = html;
            div.appendChild(chkbox_label);
            return div;
        };
        const chkbox_sp = create_chkbox("config-setting-sp", "SPONSOR_PRODUCT_VISIBLE", "スポンサープロダクトを<strong>表示する</strong>");
        const chkbox_ad = create_chkbox("config-setting-ad", "ADVERTISEMENT_SPACE_VISIBLE", "検索上部の広告を<strong>表示する</strong>");
        const chkbox_fa = create_chkbox("config-setting-fa", "FEATURED_ADVISER_VISIBLE", "FEATURED ADVISER(おすすめ等)を<strong>表示する</strong>");
        const chkbox_oneclick = create_chkbox("config-setting-oneclick", "ONECLICK_BUTTON_VISIBLE", "1-Click購入ボタンを<strong>表示する</strong>");
        const chkbox_price = create_chkbox("config-setting-price", "PRICE_EMPHASIS_OFF", "目立たない価格やポイントの<strong>強調をやめる</strong>");
        const chkbox_kdp = create_chkbox("config-setting-kdp", "PERHAPSKDP_HIDE", "Kindle注文済みスキャンにKDP疑いを<strong>含める</strong><br/>※青空文庫などをKDP疑いと誤判定するので注意");
        const chkbox_kakusu = create_chkbox("config-setting-kakusu", "KAKUSU_BUTTON_HIDE", "「非表示設定」ボタンを<strong>表示しない</strong>");

        // ダイアログを閉じるボタン
        const button_close = document.createElement("button");
        button_close.type = "button";
        button_close.textContent = "閉じる";
        button_close.addEventListener("click", (event) => {
            removeElement(document.getElementById("config-setting"));
        });

        // 作成した要素を追加していく
        config.appendChild(caption1);
        config.appendChild(chkbox_sp);
        config.appendChild(document.createElement("br"));
        config.appendChild(chkbox_ad);
        config.appendChild(document.createElement("br"));
        config.appendChild(chkbox_fa);
        config.appendChild(document.createElement("br"));
        config.appendChild(chkbox_oneclick);
        config.appendChild(document.createElement("br"));
        config.appendChild(chkbox_price);
        config.appendChild(document.createElement("br"));
        config.appendChild(chkbox_kdp);
        config.appendChild(document.createElement("br"));
        config.appendChild(chkbox_kakusu);
        config.appendChild(document.createElement("br"));
        config.appendChild(button_close);

        // #oshirase-outerに追加
        const outer = getElementOshiraseOuter();
        outer.appendChild(config);
    };

    ////////////////////////////////
    // キャッシュ管理
    const configHideCcahe = () => {
        let config = document.getElementById("config-kakusu-cache");
        if(config) return; // すでに表示されいてる

        // 要素作成
        config = document.createElement("div");
        config.id = "config-kakusu-cache";
        // キャプション部分
        const caption1 = document.createElement("span");
        const caption2 = document.createElement("span");
        caption1.textContent = "非表示中のASIN一覧(コピペで手動エクスポート)";
        caption2.textContent = "追加するASIN(インポートの代わり)";
        // テキストエリア
        const texta1 = document.createElement("textarea");
        const texta2 = document.createElement("textarea");
        texta1.id = "config-kakusu-cache-textarea1";
        texta2.id = "config-kakusu-cache-textarea2";
        // 追加ボタン
        const button_add = document.createElement("button");
        button_add.type = "button";
        button_add.textContent = "上記ASINを追加";
        // 古いキャッシュ削除ボタン
        const button_expiration = document.createElement("button");
        let deadline_str = "";
        if(CACHE_TIME >= 365*24*60*60*1000){
            deadline_str = "約" + (CACHE_TIME/(365*24*60*60*1000.0)).toFixed(1) + "年";
        }else if(CACHE_TIME >= 24*60*60*1000){
            deadline_str = (CACHE_TIME/(24*60*60*1000.0)).toFixed(1) + "日";
        }else if(CACHE_TIME >= 60*60*1000){
            deadline_str = (CACHE_TIME/(60*60*1000.0)).toFixed(1) + "時間";
        }else{
            deadline_str = (CACHE_TIME/(60*1000.0)).toFixed(1) + "分";
        }
        button_expiration.textContent = deadline_str + "以上参照のないものを削除";
        // 閉じるボタン
        const button_close = document.createElement("button");
        button_close.type = "button";
        button_close.textContent = "閉じる";

        // キャッシュのキーからASINを抽出してテキストエリアに列挙
        let str_asins = "";
        const slice_begin = "NGASIN".length; // 接頭文字の長さ
        for(const key in localStorage){
            if(/^NGASIN[0-9A-Za-z]{10}$/.test(key)){
                str_asins += key.slice(slice_begin) + "\n";
            }
        }
        texta1.value = str_asins;
        texta1.readOnly = true;
        // テキストエリアにフォーカスが当たったとき全選択
        texta1.addEventListener("focus", () => {
            texta1.select();
        });

        // 追加ボタンの関数(テキストエリアのASINをキャッシュに追加)
        button_add.addEventListener("click", (event) => {
            const config = document.getElementById("config-kakusu-cache");
            const texta2 = document.getElementById("config-kakusu-cache-textarea2");
            if(!config || !texta2){
                removeElement(texta2);
                removeElement(config);
                return;
            }
            let add_count = 0;
            for(const asin of texta2.value.split(/\s+/)){
                if(/^[0-9A-Za-z]{10}$/.test(asin)){
                    setHideCcahe(asin);
                    add_count++;
                }
            }
            removeElement(texta2);
            removeElement(config);

            // ポップ表示
            popup(add_count + "件追加(もしくはタイムスタンプを更新)しました", true);
        });
        // 古いキャッシュ削除ボタンの関数
        button_expiration.addEventListener("click", (event) => {
            const config = document.getElementById("config-kakusu-cache");
            if(!config) return;
            const result = expirationLocalCache();
            removeElement(config);

            // ポップ表示
            popup(result[0] + "件のキャッシュのうち" + result[1] + "件を削除", true);
        });
        // 閉じるボタンの関数(ダイアログを閉じる)
        button_close.addEventListener("click", (event) => {
            removeElement(document.getElementById("config-kakusu-cache"));
        });

        // 作成した要素を追加していく
        config.appendChild(caption1);
        config.appendChild(texta1);
        config.appendChild(caption2);
        config.appendChild(texta2);
        config.appendChild(button_add);
        config.appendChild(document.createElement("br"));
        config.appendChild(button_expiration);
        config.appendChild(document.createElement("br"));
        config.appendChild(button_close);
        // #oshirase-outerに追加
        const outer = getElementOshiraseOuter();
        outer.appendChild(config);
    };

    ////////////////////////////////
    // ローカルストレージのキャッシュを全表示(デバッグ用)
    const viewLS = () => {
        const lag2str = (t) => {
            if(t<0) return "未来";
            const col = (t >= CACHE_TIME) ? '<span style="color:red">' : '<span>';
            t = Math.floor(t / 1000);
            const sec = t % 60;
            t = Math.floor(t / 60);
            const min = t % 60;
            t = Math.floor(t / 60);
            const hour = t % 24;
            t = Math.floor(t / 24);
            const day = t;
            return col +
                (day>0?day+"日":"") +
                (hour>0?hour+"時間":"") +
                (min>0?min+"分":"") +
                (sec>0?sec+"秒":"") +
                "前</span>";
        }
        let items = [];
        const slice_begin = "NGASIN".length;
        for(const key in localStorage){
            if(/^NGASIN[0-9A-Za-z]{10}$/.test(key)){
                const asin = key.slice(slice_begin);
                items.push({"asin": asin, "timestamp": Number(localStorage.getItem(key))});
            }
        }
        items.sort((a,b)=>{
            if(a.timestamp > b.timestamp) return 1;
            if(a.timestamp < b.timestamp) return -1;
            if(a.asin > b.asin) return 1;
            if(a.asin < b.asin) return -1;
            return 0;
        });
        let str = Object.keys(items).length + "件<br/>";
        const now_time = Date.now();
        for(const item of items){
            str += item.asin + " : " + item.timestamp + "(" + lag2str(now_time-item.timestamp) + ")<a target=\"_blank\" href=\"https://www.amazon.co.jp/dp/" + item.asin + "\">link</a><br/>";
        }
        document.getElementsByTagName("body")[0].innerHTML = str;
    };

    // メニューコマンドに登録
    GM_registerMenuCommand("非表示/確認", displayHide);
    GM_registerMenuCommand("Kindle注文済みスキャン", scanBought);
    GM_registerMenuCommand("全部非表示", hideAll);
    GM_registerMenuCommand("Undo", undo);
    GM_registerMenuCommand("追加設定", configSetting);
    GM_registerMenuCommand("NGASIN管理", configHideCcahe);
    //GM_registerMenuCommand("NGASIN一覧表示", viewLS);
    //GM_registerMenuCommand("注文済みキャッシュ消去", clearHideCcahe);
    //GM_registerMenuCommand("スキャン済みキャッシュ消去", clearScannedCache);

    // DOM監視開始
    (new MutationObserver(hideNGASIN)).observe(target, {childList:true, subtree:true});

    // 非表示処理
    hideNGASIN(null);
})();