ebaytotalprice-userscript

Add the total eBay auction price including postage in the auction listing

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name         ebaytotalprice-userscript
// @namespace    https://github.com/subz390
// @version      2.3.3.230215200537
// @description  Add the total eBay auction price including postage in the auction listing
// @author       SubZ390
// @license      MIT
// @run-at       document-idle
// @grant        none
// @noframes
// @include      /^https?://(ar|b[ory]|c[lor]|do|ec|gt|hn|il|kz|mx|ni|p[aerty]|ru|sv|uy|ve|www)\.ebay\.com/
// @include      /^https?://www\.ebay\.com\.au/
// @include      /^https?://www\.ebay\.co\.uk/
// @include      /^https?://www\.ebay\.(at|ca|de|es|fr|ie|it|nl|ph|pl)/
// @include      /^https?://www\.be(nl|fr)\.ebay\.be/
//
//
// ==/UserScript==

function waitForMini({tryFor = 3, every = 100, test = () => false, success = () => null, timeout = () => null} = {}) {
  function leadingEdge() {
    const testResult = test();
    if (testResult) {
      success(testResult);
      return true
    }
    return false
  }
  if (leadingEdge() === false) {
    const intervalReference = setInterval(() => {
      const testResult = test();
      if (testResult) {
        clearInterval(intervalReference);
        clearTimeout(setTimeoutReference);
        success(testResult);
      }
    }, every);
    const setTimeoutReference = setTimeout(() => {
      clearInterval(intervalReference);
      timeout();
    }, tryFor * 1000);
  }
}

function realTypeOf(object, lowerCase = true) {
  if (typeof object !== 'object') return typeof object
  if (object === null) return 'null'
  if (Array.isArray(object)) return 'array'
  const internalClass = Object.prototype.toString.call(object).slice(8, -1);
  return lowerCase === true ? internalClass.toLowerCase() : internalClass
}

function getNode(node = '', debug = undefined, scope = document) {
  try {
    scope = scope === null ? document : scope;
    const nodeType = realTypeOf(node);
    if (nodeType == 'string') {
      if (node == '') {return null}
      let scopeType = realTypeOf(scope);
      if (scopeType == 'text') {
        return null
      }
      if (scopeType == 'string') {
        const tempScope = document.querySelector(scope);
        if (tempScope == null) {
          return null
        }
        scope = tempScope;
      }
      scopeType = realTypeOf(scope);
      if (scopeType.search(/array|nodelist|svgsvgelement|html|document/i) !== -1) {
        nodeType;
        const element = scope.querySelector(node);
        return element
      }
      else {
        return null
      }
    }
    else if (nodeType.search(/array|nodelist|svgsvgelement|html/i) !== -1) {
      return node
    }
    else if (nodeType.search(/null/) !== -1) {
      return null
    }
    else {
      return null
    }
  }
  catch (error) {
    console.error(error);
  }
}

function appendStyle({style: styleString, className = undefined, whereAdjacent = 'afterend', whereTarget = 'body', tryFor = 5, failMessage = undefined}) {
  return new Promise((resolve, reject) => {
    const styleElement = document.createElement('style');
    styleElement.appendChild(document.createTextNode(styleString));
    if (className) {styleElement.className = className;}
    function appendTarget(targetNode, styleElement) {
      if (whereAdjacent !== undefined) {
        return targetNode.insertAdjacentElement(whereAdjacent, styleElement)
      }
      else {
        return targetNode.appendChild(styleElement)
      }
    }
    waitForMini({
      tryFor: tryFor,
      every: 100,
      test: () => getNode(whereTarget),
      success: (targetNode) => {resolve(appendTarget(targetNode, styleElement));},
      timeout: () => reject(Error(failMessage || `appendStyle timed out whilst waiting for targetNode: ${whereTarget}`))
    });
  })
}

function findMatch(string, regex, index = 1) {
  if (string === null) return null
  const m = string.match(regex);
  return (m) ? (index=='all' ? m : (m[index] ? m[index] : m[0])) : null
}

function sprintf2({template = '', regex = /{([^{}]+)}/g, values} = {}) {
  if (template === '') {
    console.warn('template is an empty string');
    return null
  }
  function templateReplace(replaceTemplate, replaceValues) {
    return replaceTemplate.replace(regex, (match, name) => {
      if (replaceValues[name]) {
        if (typeof replaceValues[name] === 'function') {
          return replaceValues[name]().toString()
        }
        return replaceValues[name] || match
      }
      if (replaceValues[name] === 0) {
        return replaceValues[name].toString()
      }
      if (typeof replaceValues[name] === 'string' && replaceValues[name].length == 0) {
        return ''
      }
      return match
    })
  }
  if (Array.isArray(values)) {
    values.forEach((object) => {template = templateReplace(template, object);});
    return template
  }
  else {
    return templateReplace(template, values)
  }
}

function qs({selector = null, scope = document, array = false, all = false, contains = null, unittest = false, debugTag = ''} = {}) {
  const language = {
    en: {
      selectorUndefined: `${debugTag}selector is undefined`,
      scopeNotUseable: `${debugTag}scope is not useable`,
    }
  };
  if (unittest === 'language') {return language}
  try {
    if (selector === null) {
      console.error(language.en.selectorUndefined);
      return null
    }
    if (scope !== document) {
      scope = getNode(scope);
      if (scope === null) {
        return null
      }
    }
    if (unittest === 'scope') {return scope}
    if (unittest === 'options') {
      return {
        selector: selector,
        scope: scope,
        array: array,
        all: all,
        contains: contains,
        unittest: unittest
      }
    }
    if (all === true) {
      const staticNodeList = scope.querySelectorAll(selector);
      if (staticNodeList.length === 0) {return null}
      if (array === true) {
        if (contains !== null) {
          const tempArray = [];
          staticNodeList.forEach((element) => {
            if (element.textContent.search(contains) !== -1) {
              tempArray.push(element);
            }
          });
          if (tempArray.length === 0) {return null}
          else {return tempArray}
        }
        return Array.from(staticNodeList)
      }
      else {
        if (contains !== null) {
          for (let index = 0; index < staticNodeList.length; index++) {
            if (staticNodeList[index].textContent.search(contains) !== -1) {
              return staticNodeList
            }
          }
          return null
        }
        return staticNodeList
      }
    }
    else {
      const qsHTMLElement = scope.querySelector(selector);
      if (qsHTMLElement === null) {return null}
      if (typeof contains === 'string' || contains instanceof RegExp) {
        if (qsHTMLElement.textContent.search(contains) === -1) {return null}
      }
      if (array === true) {return [qsHTMLElement]}
      else {return qsHTMLElement}
    }
  }
  catch (error) {
    console.error(error);
  }
}

const globals = {
  priceMatchRegExp: /((\d+[,\.])+\d+)/,
  currencySymbolsRegExp: /(\$|EUR|PHP|zł|£)/,
  itemPriceElementTemplate: '<span class="total-price">{currencySymbol}{totalPrice}</span>',
  itemPriceElementTemplateSelector: 'span.total-price',
  itemPriceElementInnerTextTemplate: '{currencySymbol}{totalPrice}',
};

function processMethod(options) {
  try {
    function getMethod() {
      for (const [type, method] of Object.entries(options)) {
        for (let index = 0; index < method.identifierSelector.length; index++) {
          const selector = method.identifierSelector[index];
          const identifierNode = getNode(selector);
          if (identifierNode !== null) {return method}
        }
      }
      return null
    }
    const method = getMethod(options);
    if (method !== null) {method.process();}
  }
  catch (error) {console.error(error);}
}

function getValue(element) {
  try {
    let value = findMatch(element.textContent.trim(), globals.priceMatchRegExp);
    value = value.replace(/[,\.]/g, '');
    value = parseFloat(value);
    return value
  }
  catch (error) {
    console.error(error);
    return null
  }
}

function findParent({child, contains = null}) {
  let parentNodeElement = child;
  for (let i = 1; parentNodeElement.isEqualNode(document) === false; i++) {
    if (parentNodeElement.textContent.search(contains) !== -1) {
      return parentNodeElement
    }
    parentNodeElement = parentNodeElement.parentNode;
  }
  return null
}

function processItemListing({listItemsSelector, itemPriceElementSelector, convertPriceElementSelector, itemPriceElementTemplate = null, itemShippingElementSelector, convertShippingElementSelector, itemShippingElementTemplate = null}) {
  const content = qs({selector: listItemsSelector});
  if (content) {
    const itemPriceElement = qs({selector: convertPriceElementSelector, scope: content, contains: /\d/}) || qs({selector: itemPriceElementSelector, scope: content, contains: /\d/});
    let itemShippingElement = qs({selector: convertShippingElementSelector, scope: content, contains: /\d/}) || qs({selector: itemShippingElementSelector, scope: content, contains: /\d/});
    if (itemShippingElement === null) {
      const postageSpan = qs({selector: 'span', scope: content, contains: 'Postage:', all: true, array: true});
      itemShippingElement = findParent({child: postageSpan[0], contains: /\d/});
    }
    if (itemPriceElement && itemShippingElement) {
      const priceCurrencySymbol = findMatch(itemPriceElement.textContent.trim(), globals.currencySymbolsRegExp);
      const shippingCurrencySymbol = findMatch(itemShippingElement.textContent.trim(), globals.currencySymbolsRegExp);
      if (shippingCurrencySymbol && (shippingCurrencySymbol === priceCurrencySymbol)) {
        const totalPrice = ((getValue(itemPriceElement) + getValue(itemShippingElement)) / 100).toFixed(2);
        const HTML = sprintf2({
          template: itemShippingElementTemplate || itemPriceElementTemplate,
          values: {
            itemPrice: itemPriceElement.textContent.trim(),
            itemShippingAmount: itemShippingElement.textContent.trim(),
            currencySymbol: shippingCurrencySymbol,
            totalPrice: totalPrice
          }
        });
        if (itemPriceElementTemplate) {
          itemPriceElement.insertAdjacentHTML('afterend', HTML);
        }
        else {
          itemShippingElement.innerHTML = HTML;
        }
        const itemPriceElementObserver = new MutationObserver((mutationList, observer) => {
          mutationList.forEach((mutation) => {
            mutation.addedNodes.forEach((element) => {
              if (element.nodeName == '#text') {
                const totalPriceElement = getNode(globals.itemPriceElementTemplateSelector);
                const totalPrice = ((getValue(itemPriceElement) + getValue(itemShippingElement)) / 100).toFixed(2);
                const totalPriceText = sprintf2({
                  template: globals.itemPriceElementInnerTextTemplate,
                  values: {
                    itemPrice: itemPriceElement.textContent.trim(),
                    itemShippingAmount: itemShippingElement.textContent.trim(),
                    currencySymbol: shippingCurrencySymbol,
                    totalPrice: totalPrice
                  }
                });
                totalPriceElement.textContent = totalPriceText;
              }
            });
          });
        });
        itemPriceElementObserver.observe(itemPriceElement, {childList: true});
      }
    }
  }
}

function processListGallery({listItemsSelector, itemPriceElementSelector, itemPriceElementTemplate = null, itemShippingElementSelector, itemShippingElementTemplate = null}) {
  const listItems = qs({selector: listItemsSelector, all: true, array: true});
  if (listItems) {
    for (let i = 0; listItems[i]; i++) {
      const itemPriceElement = qs({selector: itemPriceElementSelector, scope: listItems[i]});
      const itemShippingElement = qs({selector: itemShippingElementSelector, scope: listItems[i], contains: /\d/});
      if (itemPriceElement && itemShippingElement) {
        const priceCurrencySymbol = findMatch(itemPriceElement.textContent.trim(), globals.currencySymbolsRegExp);
        const shippingCurrencySymbol = findMatch(itemShippingElement.textContent.trim(), globals.currencySymbolsRegExp);
        if (shippingCurrencySymbol && (shippingCurrencySymbol === priceCurrencySymbol)) {
          const totalPrice = ((getValue(itemPriceElement) + getValue(itemShippingElement)) / 100).toFixed(2);
          const HTML = sprintf2({
            template: itemShippingElementTemplate || itemPriceElementTemplate,
            values: {
              itemPrice: itemPriceElement.textContent.trim(),
              itemShippingAmount: itemShippingElement.textContent.trim(),
              currencySymbol: shippingCurrencySymbol,
              totalPrice: totalPrice
            }
          });
          if (itemPriceElementTemplate) {
            itemPriceElement.insertAdjacentHTML('afterend', HTML);
          }
          else {
            itemShippingElement.innerHTML = HTML;
          }
        }
      }
    }
  }
}

var stylesheet = ".total-price{background:hsl(76,100%,50%)!important;color:hsl(0,0%,9%)!important;padding:1px 4px;margin-left:5px;font-size:20px!important;font-weight:400!important;}.s-item__detail{overflow:visible!important;}";

appendStyle({style: stylesheet});
processMethod({
  search: {
    identifierSelector: ['#mainContent ul.srp-results', '#mainContent ul.b-list__items_nofooter'],
    process: () => processListGallery({
      listItemsSelector: '#mainContent li.s-item',
      itemPriceElementSelector: '.s-item__price',
      itemShippingElementSelector: '.s-item__shipping',
      itemPriceElementTemplate: globals.itemPriceElementTemplate
    })
  },
  sch: {
    identifierSelector: ['#mainContent ul#ListViewInner'],
    process: () => processListGallery({
      listItemsSelector: '#mainContent li',
      itemPriceElementSelector: '.lvprice span',
      itemShippingElementSelector: '.lvshipping span.fee',
      itemPriceElementTemplate: globals.itemPriceElementTemplate
    })
  },
  itm: {
    identifierSelector: ['#mainContent form[name="viactiondetails"]'],
    process: () => processItemListing({
      listItemsSelector: '#mainContent',
      itemPriceElementSelector: 'span[itemprop="price"]',
      convertPriceElementSelector: '#prcIsumConv',
      itemShippingElementSelector: 'div[class*="shipping"]',
      convertShippingElementSelector: '#convetedPriceId',
      itemPriceElementTemplate: globals.itemPriceElementTemplate
    })
  }
});