brickmerge® Prices

Displays lowest brickmerge® price next to offer price

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey, Greasemonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

You will need to install an extension such as Tampermonkey to install this script.

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name           brickmerge® Prices
// @name:de        brickmerge® Preise
// @namespace      https://brickmerge.de/
// @version        1.27.4
// @license        MIT
// @description    Displays lowest brickmerge® price next to offer price
// @description:de Zeigt den bisherigen Bestpreis von brickmerge® parallel zum aktuellen Preis an
// @author         Philipp Kursawe <[email protected]>
// @match          https://www.alternate.de/LEGO/*
// @match          https://www.alternate.de/html/product/*
// @match          https://www.alza.de/spielzeug/lego-*
// @match          https://www.alza.at/spielzeug/lego-*
// @match          https://www.amazon.*/LEGO-*
// @match          https://www.amazon.*/*LEGO*
// @match          https://www.amazon.*/*p/*
// @match          https://www.amazon.*/*/*p/*
// @match          https://www.bol.de/shop/home/artikeldetails/*
// @match          https://www.digitalo.de/products/*/*-LEGO-*
// @match          https://www.ebay.de/itm/*
// @match          https://www.galeria.de/produkt/lego-*
// @match          https://www.jb-spielwaren.de/*
// @match          https://www.kleinanzeigen.de/s-anzeige/lego-*
// @match          https://www.lego.com/de-de/product/*
// @match          https://www.mediamarkt.de/de/product/_lego-*
// @match          https://www.mueller.de/p/lego-*
// @match          https://www.otto.de/p/lego-*
// @match          https://www.proshop.de/LEGO/*
// @match          https://www.shopdisney.de/lego-*
// @match          https://www.saturn.de/de/product/_lego-*
// @match          https://www.smythstoys.com/de/de-de/spielzeug/lego/*
// @match          https://steinehelden.de/*
// @match          https://www.thalia.de/shop/home/artikeldetails/*
// @match          https://www.toys-for-fun.com/de/lego*
// @match          https://www.toymi.eu/LEGO-*
// @icon           https://www.google.com/s2/favicons?sz=64&domain=brickmerge.de
// @homepageURL	   https://github.com/pke/brickmerge-userscript
// @supportURL     https://github.com/pke/brickmerge-userscript/discussions
// @run-at         document-end
// @grant          GM_xmlhttpRequest
// @grant          GM_info
// @connect        hypermedia.rocks
// @connect        *
// ==/UserScript==

(function() {
    'use strict';
    'use esversion:11';

    // In tests this function is not defined and can't be injected via Page.evaluate nor Page.evaluateOnNewDocument
    if (!window.GM_xmlhttpRequest) {
      window.GM_xmlhttpRequest = function({ url, onload, headers }) {
        fetch(url, { headers })
          .then(response => response.text())
          .then(responseText => onload({ responseText }))
      }
    }

    if (!window.GM_info) {
      window.GM_info = {
        scriptHandler: "Violentmonkey",
        version: "4.0.0",
        script: {
          version: "1.27.0",
        }
      }
    }

    const style = `
     .brickmerge-price {
       background-color: #b00 !important;
       color: #fff !important;
       margin: 1rem 0 !important;
       padding: 0.3rem 0.5rem !important;
     }
     .brickmerge-price a {
       color: #fff !important;
       font-weight: bold !important;
       text-decoration: underline !important;
     }
     .brickmerge-price a:hover {
       text-decoration: none !important;
     }
     .brickmerge-price img {
       height: 16px;
       display: inline;
       vertical-align: middle;
       margin-right: 0.3rem;
     }
     .brickmerge-price img.small {
       width: 16px;
     }`;

    const logo = `https://raw.githubusercontent.com/pke/brickmerge-userscript/master/public/images/brickmerge.svg`;

    const resolvers = {
        "www.amazon.de": {
            targetSelector: "#corePriceDisplay_desktop_feature_div,#corePrice_feature_div",
            testURL: "https://www.amazon.de/LEGO-43230-Disney-Kamera-Maus-Minifiguren/dp/B0BV7BMPVS",
        },
        "www.amazon.fr": "www.amazon.de",
        "www.amazon.es": "www.amazon.de",
        "www.smythstoys.com": {
            targetSelector: "#product-info div[itemprop=price]",
            testURL: "https://www.smythstoys.com/de/de-de/spielzeug/lego/lego-fuer-erwachsene/lego-icons-set-10266-nasa-apollo-11-mondlandefaehre/p/183613",
        },
        "www.toys-for-fun.com": {
            targetSelector: ".product-info-price",
            testURL: "https://www.toys-for-fun.com/de/legor-disney-43230-kamera-hommage-an-walt-disney.html",
        },
        "www.jb-spielwaren.de": {
            targetSelector: ".widget-availability",
            testURL: "https://www.jb-spielwaren.de/lego-10293-besuch-des-weihnachtsmanns/a-10293/",
        },
        "steinehelden.de": {
            articleExtractor: /(\d{4,})/,
            targetSelector: "div[itemprop=offers] .product--tax",
            testURL: "https://steinehelden.de/city-arktis-schneemobil-60376/",
        },
        "www.proshop.de": {
            targetSelector: "#site-product-price-stock-buy-container span.site-currency-wrapper",
            testURL: "https://www.proshop.de/LEGO/LEGO-Ideas-21343-Wikingerdorf/3195765",
        },
        "www.alternate.de": {
          parent: true,
          prepend: true,
          targetSelector: "#product-top-right .price",
          testURL: "https://www.alternate.de/LEGO/10311-Creator-Expert-Orchidee-Konstruktionsspielzeug/html/product/1818749",
        },
        "www.saturn.de": {
            targetSelector: "div[data-test='mms-pdp-offer-selection']",
            prepend: true,
            dynamic: "h1", // Site changes its DOM via script and could remove our element
            styleSelector: "div[data-test='mms-branded-price'] p > span",
            style(element) {
                element.style = "text-align: right";
            },
            testURL: "https://www.saturn.de/de/product/_lego-10281-bonsai-baum-2672008.html",
        },
        "www.mediamarkt.de": "www.saturn.de", // just an alias, same as saturn
        "www.otto.de": {
            targetSelector: ".pdp_price__inner",
            prepend: true,
            testURL: "https://www.otto.de/p/lego-konstruktionsspielsteine-kamera-hommage-an-walt-disney-43230-lego-disney-811-st-made-in-europe-C1725197870/#variationId=1725014125",
        },
        "www.mueller.de": {
            targetSelector: ".mu-product-price.mu-product-details-page__price",
            testURL: "https://www.mueller.de/p/lego-icons-10281-bonsai-baum-kunstpflanzen-set-fuer-erwachsene-deko-2681620/",
        },
        "www.thalia.de": {
            targetSelector: "artikel-informationen",
            style(element) {
                element.classList.add("element-text-small");
            },
            testURL: "https://www.thalia.de/shop/home/artikeldetails/A1068002914",
        },
        "www.bol.de": "www.thalia.de", // https://www.bol.de/shop/home/artikeldetails/A1066075411
        "www.ebay.de": {
            parent: true,
            prepend: true,
            style: "text-align: center",
            targetSelector: ".x-bin-price",
            testURL: "https://www.ebay.de/itm/204515604952",
        },
        "www.alza.de": {
            parent: true,
            prepend: true,
            targetSelector: ".price-detail__row",
            testURL: "https://www.alza.de/spielzeug/lego-disney-43230-kamera-hommage-an-walt-disney-d7744520.htm",
        },
        "www.alza.at": "www.alza.de",
        "www.kleinanzeigen.de": {
            parent: true,
            prepend: true,
            targetSelector: "#viewad-title,.ad-keydetails--price-and-shipping",
        },
        "www.digitalo.de": {
            parent: true,
            prepend: true,
            articleExtractor: /(\d{4,}) LEGO/,
            targetSelector: ".large_order_5",
        },
        "www.galeria.de": {
            targetSelector: "div[data-testid=productDetails] h1",
        },
        "www.shopdisney.de": {
            targetSelector: "div.prices",
        },
        "www.lego.com": {
            articleExtractor: /(\d{4,})/,
            parent: true,
            dynamic: true,
            targetSelector: "div[class^='ProductOverviewstyles__PriceAvailabilityWrapper-'] span[data-test='product-price']",
        },
        "www.toymi.eu": {
            targetSelector: ".price.h1",
            parent: true,
            prepend: true,
        },
    };

    function renderError(element, error, operation = "append") {
        if (!element) {
            return;
        }
        const errorElement = document.createElement("div");
        errorElement.innerText = error.message;
        element[operation]?.(errorElement);
    }

    function addLowestPrice(element, title = "Bestpreis wird geladen", url, lowestPrice, operation = "append", icon, iconClass = []) {
        if (!element) {
            return;
        }
        let brickmergeBox = element.querySelector(".brickmerge-price");
        let isNew = !brickmergeBox;
        // console.log("addLowestPrice isNew: ", isNew);
        if (!brickmergeBox) {
            brickmergeBox = document.createElement("div");
            brickmergeBox.className = "brickmerge-price";
        }
        const brickmergeLink = url ? `<a href="${url}">${lowestPrice}</a>` : "...";
        const iconImage = icon ? `<img src="${icon}" class="${iconClass.join(" ")}"/>` : "";
        brickmergeBox.innerHTML = `${iconImage}${title}: ${brickmergeLink}`;
        if (isNew) {
            element[operation]?.(brickmergeBox);
        }
        return brickmergeBox;
    }

    function addPriceToTargets(resolver, priceOrError, url, styleClasses, title, icon, iconClass) {
        const wait = (resolver.dynamic && priceOrError !== "...") ? new Promise(resolve => setTimeout(resolve, 1000)) : Promise.resolve()
        wait.then(() => {
          const targets = document.querySelectorAll(resolver.targetSelector);
          if (targets.length === 0) {
              // console.log(`Target ${resolver.targetSelector} not found`);
              return;
          }
          if (!document.querySelector("head style.brickmerge")) {
              const styleElement = document.createElement("style");
              styleElement.className = "brickmerge";
              styleElement.type = 'text/css';
              styleElement.innerHTML = style;
              document.head.appendChild(styleElement);
          }
          const error = priceOrError instanceof Error && priceOrError
          const price = error ? undefined : priceOrError
          if (error instanceof Error) {
              for (let element of targets) {
                  if (resolver.parent) {
                      element = element.parentElement
                  }
                  renderError(element, error, resolver.prepend ? "prepend" : "append");
              }
          } else if (price) {
              for (let element of targets) {
                  if (resolver.parent) {
                      element = element.parentElement
                  }
                  //console.log("target:", element.innerHTML);
                  const box = addLowestPrice(element, title, url, price, resolver.prepend ? "prepend" : "append", icon, iconClass);
                  if (styleClasses) {
                      box.classList.add(...styleClasses.split(" "));
                  }
                  if (typeof resolver.style === "function") {
                      resolver.style(box);
                  } else if (typeof resolver.style === "string") {
                      box.style = resolver.style;
                  }
              }
          }
        })
    }

    let resolver = resolvers[document.location.host]
    // Do we have an alias for another resolver?
    if (typeof resolver === "string") {
        resolver = resolvers[resolver];
    }
    if (!resolver) {
        return;
    }

    const styleNode = document.querySelector(resolver.styleSelector);
    // console.log("styleNode", styleNode);
    const styleClasses = styleNode?.className;

    function getSetNumber() {
      const [, setNumber] = (resolver.articleExtractor || /LEGO.*?(\d{4,})/i).exec(document.title) || [];
      return setNumber
    }

    function fetchPrice() {
      addPriceToTargets(resolver, "...", "", styleClasses);

      GM_xmlhttpRequest({
          url: "https://brickmerge-userscript.hypermedia.rocks/lowest/" + getSetNumber(),
          headers: {
            "User-Agent": `${navigator.userAgent} brickmerge/${GM_info.script.version} ${GM_info.scriptHandler}/${GM_info.version}`,
          },
          onload(response) {
            const json = JSON.parse(response.responseText);
            const { title, links } = json;
            const icon = links.find(link => link.rel.includes("icon")) || { href: logo };
            const link = links.find(link => link.rel.includes("self"));
            addPriceToTargets(resolver, link.title, link.href, styleClasses, title, icon.href, icon.class);
          }
      });
    }

    if (!getSetNumber()) {
        return;
    }

    if (resolver.dynamic) {
      new MutationObserver(function(mutations) {
          console.log(mutations[0].target.nodeValue);
          fetchPrice();
      }).observe(
          document.querySelector('title'),
          { subtree: true, characterData: true, childList: true }
      );
    }
        // Create an observer instance linked to the callback function
        /*const observer = new MutationObserver((mutations) => {
            for (const mutation of mutations) {
                if (mutation.type === "characterData") {
                    if (mutation.target.querySelector?.(resolver.targetSelector)) {
                        fetchPrice();
                    }
                } else if (mutation.type === "childList" && mutation.addedNodes?.length) {
                    for (const addedNode of mutation.addedNodes) {
                        if (addedNode.querySelector?.(resolver.targetSelector)) {
                            fetchPrice();
                        }
                    }
                }
            }
        });
        observer.observe(document.body, { characterData: true, childList: true, subtree: true });
    }*/
    fetchPrice();
})();