ebaytotalprice-userscript

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

  1. // ==UserScript==
  2. // @name ebaytotalprice-userscript
  3. // @namespace https://github.com/subz390
  4. // @version 2.3.3.230215200537
  5. // @description Add the total eBay auction price including postage in the auction listing
  6. // @author SubZ390
  7. // @license MIT
  8. // @run-at document-idle
  9. // @grant none
  10. // @noframes
  11. // @include /^https?://(ar|b[ory]|c[lor]|do|ec|gt|hn|il|kz|mx|ni|p[aerty]|ru|sv|uy|ve|www)\.ebay\.com/
  12. // @include /^https?://www\.ebay\.com\.au/
  13. // @include /^https?://www\.ebay\.co\.uk/
  14. // @include /^https?://www\.ebay\.(at|ca|de|es|fr|ie|it|nl|ph|pl)/
  15. // @include /^https?://www\.be(nl|fr)\.ebay\.be/
  16. //
  17. //
  18. // ==/UserScript==
  19.  
  20. function waitForMini({tryFor = 3, every = 100, test = () => false, success = () => null, timeout = () => null} = {}) {
  21. function leadingEdge() {
  22. const testResult = test();
  23. if (testResult) {
  24. success(testResult);
  25. return true
  26. }
  27. return false
  28. }
  29. if (leadingEdge() === false) {
  30. const intervalReference = setInterval(() => {
  31. const testResult = test();
  32. if (testResult) {
  33. clearInterval(intervalReference);
  34. clearTimeout(setTimeoutReference);
  35. success(testResult);
  36. }
  37. }, every);
  38. const setTimeoutReference = setTimeout(() => {
  39. clearInterval(intervalReference);
  40. timeout();
  41. }, tryFor * 1000);
  42. }
  43. }
  44.  
  45. function realTypeOf(object, lowerCase = true) {
  46. if (typeof object !== 'object') return typeof object
  47. if (object === null) return 'null'
  48. if (Array.isArray(object)) return 'array'
  49. const internalClass = Object.prototype.toString.call(object).slice(8, -1);
  50. return lowerCase === true ? internalClass.toLowerCase() : internalClass
  51. }
  52.  
  53. function getNode(node = '', debug = undefined, scope = document) {
  54. try {
  55. scope = scope === null ? document : scope;
  56. const nodeType = realTypeOf(node);
  57. if (nodeType == 'string') {
  58. if (node == '') {return null}
  59. let scopeType = realTypeOf(scope);
  60. if (scopeType == 'text') {
  61. return null
  62. }
  63. if (scopeType == 'string') {
  64. const tempScope = document.querySelector(scope);
  65. if (tempScope == null) {
  66. return null
  67. }
  68. scope = tempScope;
  69. }
  70. scopeType = realTypeOf(scope);
  71. if (scopeType.search(/array|nodelist|svgsvgelement|html|document/i) !== -1) {
  72. nodeType;
  73. const element = scope.querySelector(node);
  74. return element
  75. }
  76. else {
  77. return null
  78. }
  79. }
  80. else if (nodeType.search(/array|nodelist|svgsvgelement|html/i) !== -1) {
  81. return node
  82. }
  83. else if (nodeType.search(/null/) !== -1) {
  84. return null
  85. }
  86. else {
  87. return null
  88. }
  89. }
  90. catch (error) {
  91. console.error(error);
  92. }
  93. }
  94.  
  95. function appendStyle({style: styleString, className = undefined, whereAdjacent = 'afterend', whereTarget = 'body', tryFor = 5, failMessage = undefined}) {
  96. return new Promise((resolve, reject) => {
  97. const styleElement = document.createElement('style');
  98. styleElement.appendChild(document.createTextNode(styleString));
  99. if (className) {styleElement.className = className;}
  100. function appendTarget(targetNode, styleElement) {
  101. if (whereAdjacent !== undefined) {
  102. return targetNode.insertAdjacentElement(whereAdjacent, styleElement)
  103. }
  104. else {
  105. return targetNode.appendChild(styleElement)
  106. }
  107. }
  108. waitForMini({
  109. tryFor: tryFor,
  110. every: 100,
  111. test: () => getNode(whereTarget),
  112. success: (targetNode) => {resolve(appendTarget(targetNode, styleElement));},
  113. timeout: () => reject(Error(failMessage || `appendStyle timed out whilst waiting for targetNode: ${whereTarget}`))
  114. });
  115. })
  116. }
  117.  
  118. function findMatch(string, regex, index = 1) {
  119. if (string === null) return null
  120. const m = string.match(regex);
  121. return (m) ? (index=='all' ? m : (m[index] ? m[index] : m[0])) : null
  122. }
  123.  
  124. function sprintf2({template = '', regex = /{([^{}]+)}/g, values} = {}) {
  125. if (template === '') {
  126. console.warn('template is an empty string');
  127. return null
  128. }
  129. function templateReplace(replaceTemplate, replaceValues) {
  130. return replaceTemplate.replace(regex, (match, name) => {
  131. if (replaceValues[name]) {
  132. if (typeof replaceValues[name] === 'function') {
  133. return replaceValues[name]().toString()
  134. }
  135. return replaceValues[name] || match
  136. }
  137. if (replaceValues[name] === 0) {
  138. return replaceValues[name].toString()
  139. }
  140. if (typeof replaceValues[name] === 'string' && replaceValues[name].length == 0) {
  141. return ''
  142. }
  143. return match
  144. })
  145. }
  146. if (Array.isArray(values)) {
  147. values.forEach((object) => {template = templateReplace(template, object);});
  148. return template
  149. }
  150. else {
  151. return templateReplace(template, values)
  152. }
  153. }
  154.  
  155. function qs({selector = null, scope = document, array = false, all = false, contains = null, unittest = false, debugTag = ''} = {}) {
  156. const language = {
  157. en: {
  158. selectorUndefined: `${debugTag}selector is undefined`,
  159. scopeNotUseable: `${debugTag}scope is not useable`,
  160. }
  161. };
  162. if (unittest === 'language') {return language}
  163. try {
  164. if (selector === null) {
  165. console.error(language.en.selectorUndefined);
  166. return null
  167. }
  168. if (scope !== document) {
  169. scope = getNode(scope);
  170. if (scope === null) {
  171. return null
  172. }
  173. }
  174. if (unittest === 'scope') {return scope}
  175. if (unittest === 'options') {
  176. return {
  177. selector: selector,
  178. scope: scope,
  179. array: array,
  180. all: all,
  181. contains: contains,
  182. unittest: unittest
  183. }
  184. }
  185. if (all === true) {
  186. const staticNodeList = scope.querySelectorAll(selector);
  187. if (staticNodeList.length === 0) {return null}
  188. if (array === true) {
  189. if (contains !== null) {
  190. const tempArray = [];
  191. staticNodeList.forEach((element) => {
  192. if (element.textContent.search(contains) !== -1) {
  193. tempArray.push(element);
  194. }
  195. });
  196. if (tempArray.length === 0) {return null}
  197. else {return tempArray}
  198. }
  199. return Array.from(staticNodeList)
  200. }
  201. else {
  202. if (contains !== null) {
  203. for (let index = 0; index < staticNodeList.length; index++) {
  204. if (staticNodeList[index].textContent.search(contains) !== -1) {
  205. return staticNodeList
  206. }
  207. }
  208. return null
  209. }
  210. return staticNodeList
  211. }
  212. }
  213. else {
  214. const qsHTMLElement = scope.querySelector(selector);
  215. if (qsHTMLElement === null) {return null}
  216. if (typeof contains === 'string' || contains instanceof RegExp) {
  217. if (qsHTMLElement.textContent.search(contains) === -1) {return null}
  218. }
  219. if (array === true) {return [qsHTMLElement]}
  220. else {return qsHTMLElement}
  221. }
  222. }
  223. catch (error) {
  224. console.error(error);
  225. }
  226. }
  227.  
  228. const globals = {
  229. priceMatchRegExp: /((\d+[,\.])+\d+)/,
  230. currencySymbolsRegExp: /(\$|EUR|PHP|zł|£)/,
  231. itemPriceElementTemplate: '<span class="total-price">{currencySymbol}{totalPrice}</span>',
  232. itemPriceElementTemplateSelector: 'span.total-price',
  233. itemPriceElementInnerTextTemplate: '{currencySymbol}{totalPrice}',
  234. };
  235.  
  236. function processMethod(options) {
  237. try {
  238. function getMethod() {
  239. for (const [type, method] of Object.entries(options)) {
  240. for (let index = 0; index < method.identifierSelector.length; index++) {
  241. const selector = method.identifierSelector[index];
  242. const identifierNode = getNode(selector);
  243. if (identifierNode !== null) {return method}
  244. }
  245. }
  246. return null
  247. }
  248. const method = getMethod(options);
  249. if (method !== null) {method.process();}
  250. }
  251. catch (error) {console.error(error);}
  252. }
  253.  
  254. function getValue(element) {
  255. try {
  256. let value = findMatch(element.textContent.trim(), globals.priceMatchRegExp);
  257. value = value.replace(/[,\.]/g, '');
  258. value = parseFloat(value);
  259. return value
  260. }
  261. catch (error) {
  262. console.error(error);
  263. return null
  264. }
  265. }
  266.  
  267. function findParent({child, contains = null}) {
  268. let parentNodeElement = child;
  269. for (let i = 1; parentNodeElement.isEqualNode(document) === false; i++) {
  270. if (parentNodeElement.textContent.search(contains) !== -1) {
  271. return parentNodeElement
  272. }
  273. parentNodeElement = parentNodeElement.parentNode;
  274. }
  275. return null
  276. }
  277.  
  278. function processItemListing({listItemsSelector, itemPriceElementSelector, convertPriceElementSelector, itemPriceElementTemplate = null, itemShippingElementSelector, convertShippingElementSelector, itemShippingElementTemplate = null}) {
  279. const content = qs({selector: listItemsSelector});
  280. if (content) {
  281. const itemPriceElement = qs({selector: convertPriceElementSelector, scope: content, contains: /\d/}) || qs({selector: itemPriceElementSelector, scope: content, contains: /\d/});
  282. let itemShippingElement = qs({selector: convertShippingElementSelector, scope: content, contains: /\d/}) || qs({selector: itemShippingElementSelector, scope: content, contains: /\d/});
  283. if (itemShippingElement === null) {
  284. const postageSpan = qs({selector: 'span', scope: content, contains: 'Postage:', all: true, array: true});
  285. itemShippingElement = findParent({child: postageSpan[0], contains: /\d/});
  286. }
  287. if (itemPriceElement && itemShippingElement) {
  288. const priceCurrencySymbol = findMatch(itemPriceElement.textContent.trim(), globals.currencySymbolsRegExp);
  289. const shippingCurrencySymbol = findMatch(itemShippingElement.textContent.trim(), globals.currencySymbolsRegExp);
  290. if (shippingCurrencySymbol && (shippingCurrencySymbol === priceCurrencySymbol)) {
  291. const totalPrice = ((getValue(itemPriceElement) + getValue(itemShippingElement)) / 100).toFixed(2);
  292. const HTML = sprintf2({
  293. template: itemShippingElementTemplate || itemPriceElementTemplate,
  294. values: {
  295. itemPrice: itemPriceElement.textContent.trim(),
  296. itemShippingAmount: itemShippingElement.textContent.trim(),
  297. currencySymbol: shippingCurrencySymbol,
  298. totalPrice: totalPrice
  299. }
  300. });
  301. if (itemPriceElementTemplate) {
  302. itemPriceElement.insertAdjacentHTML('afterend', HTML);
  303. }
  304. else {
  305. itemShippingElement.innerHTML = HTML;
  306. }
  307. const itemPriceElementObserver = new MutationObserver((mutationList, observer) => {
  308. mutationList.forEach((mutation) => {
  309. mutation.addedNodes.forEach((element) => {
  310. if (element.nodeName == '#text') {
  311. const totalPriceElement = getNode(globals.itemPriceElementTemplateSelector);
  312. const totalPrice = ((getValue(itemPriceElement) + getValue(itemShippingElement)) / 100).toFixed(2);
  313. const totalPriceText = sprintf2({
  314. template: globals.itemPriceElementInnerTextTemplate,
  315. values: {
  316. itemPrice: itemPriceElement.textContent.trim(),
  317. itemShippingAmount: itemShippingElement.textContent.trim(),
  318. currencySymbol: shippingCurrencySymbol,
  319. totalPrice: totalPrice
  320. }
  321. });
  322. totalPriceElement.textContent = totalPriceText;
  323. }
  324. });
  325. });
  326. });
  327. itemPriceElementObserver.observe(itemPriceElement, {childList: true});
  328. }
  329. }
  330. }
  331. }
  332.  
  333. function processListGallery({listItemsSelector, itemPriceElementSelector, itemPriceElementTemplate = null, itemShippingElementSelector, itemShippingElementTemplate = null}) {
  334. const listItems = qs({selector: listItemsSelector, all: true, array: true});
  335. if (listItems) {
  336. for (let i = 0; listItems[i]; i++) {
  337. const itemPriceElement = qs({selector: itemPriceElementSelector, scope: listItems[i]});
  338. const itemShippingElement = qs({selector: itemShippingElementSelector, scope: listItems[i], contains: /\d/});
  339. if (itemPriceElement && itemShippingElement) {
  340. const priceCurrencySymbol = findMatch(itemPriceElement.textContent.trim(), globals.currencySymbolsRegExp);
  341. const shippingCurrencySymbol = findMatch(itemShippingElement.textContent.trim(), globals.currencySymbolsRegExp);
  342. if (shippingCurrencySymbol && (shippingCurrencySymbol === priceCurrencySymbol)) {
  343. const totalPrice = ((getValue(itemPriceElement) + getValue(itemShippingElement)) / 100).toFixed(2);
  344. const HTML = sprintf2({
  345. template: itemShippingElementTemplate || itemPriceElementTemplate,
  346. values: {
  347. itemPrice: itemPriceElement.textContent.trim(),
  348. itemShippingAmount: itemShippingElement.textContent.trim(),
  349. currencySymbol: shippingCurrencySymbol,
  350. totalPrice: totalPrice
  351. }
  352. });
  353. if (itemPriceElementTemplate) {
  354. itemPriceElement.insertAdjacentHTML('afterend', HTML);
  355. }
  356. else {
  357. itemShippingElement.innerHTML = HTML;
  358. }
  359. }
  360. }
  361. }
  362. }
  363. }
  364.  
  365. 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;}";
  366.  
  367. appendStyle({style: stylesheet});
  368. processMethod({
  369. search: {
  370. identifierSelector: ['#mainContent ul.srp-results', '#mainContent ul.b-list__items_nofooter'],
  371. process: () => processListGallery({
  372. listItemsSelector: '#mainContent li.s-item',
  373. itemPriceElementSelector: '.s-item__price',
  374. itemShippingElementSelector: '.s-item__shipping',
  375. itemPriceElementTemplate: globals.itemPriceElementTemplate
  376. })
  377. },
  378. sch: {
  379. identifierSelector: ['#mainContent ul#ListViewInner'],
  380. process: () => processListGallery({
  381. listItemsSelector: '#mainContent li',
  382. itemPriceElementSelector: '.lvprice span',
  383. itemShippingElementSelector: '.lvshipping span.fee',
  384. itemPriceElementTemplate: globals.itemPriceElementTemplate
  385. })
  386. },
  387. itm: {
  388. identifierSelector: ['#mainContent form[name="viactiondetails"]'],
  389. process: () => processItemListing({
  390. listItemsSelector: '#mainContent',
  391. itemPriceElementSelector: 'span[itemprop="price"]',
  392. convertPriceElementSelector: '#prcIsumConv',
  393. itemShippingElementSelector: 'div[class*="shipping"]',
  394. convertShippingElementSelector: '#convetedPriceId',
  395. itemPriceElementTemplate: globals.itemPriceElementTemplate
  396. })
  397. }
  398. });

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址