A Better eBay Filter

Allow filtering eBay searches based on additional criteria, global site support

  1. // abef.user.js
  2. //
  3. // Copyright 2007-2015, Michael Devore
  4. // This file is licensed under the terms of the Artistic License 2.0.
  5. // See http://opensource.org/licenses/artistic-license-2.0 for the license itself.
  6. //
  7. // This is a Greasemonkey script.
  8. // See http://www.greasespot.net/ for more information on Greasemonkey.
  9. //
  10. // ==UserScript==
  11. // @name A Better eBay Filter
  12. // @namespace http://www.devoresoftware.com/gm/abef
  13. // @description Allow filtering eBay searches based on additional criteria, global site support
  14. // @version 9.0
  15. // @include http://*listings*.ebay.*/*
  16. // @include https://*listings*.ebay.*/*
  17. // @include http://*search*.ebay.*/*
  18. // @include https://*search*.ebay.*/*
  19. // @include http://*shop*.ebay.*/*
  20. // @include https://*shop*.ebay.*/*
  21. // @include http://www.ebay.*/sch/*
  22. // @include https://www.ebay.*/sch/*
  23. // @include http://www.ebay.*/dsc/*
  24. // @include https://www.ebay.*/dsc/*
  25. // @grant GM.setValue
  26. // @grant GM.getValue
  27. // @run-at document-end
  28. // ==/UserScript==
  29. //
  30. // Version 1.0, released April 2007
  31. // Version 2.0, released late-April 2007
  32. // Version 3.0, release early January 2010, rework for revised eBay
  33. // Version 3.1, release mid-January 2010, fix for eBay Motors
  34. // Version 3.2, release early September 2010, fix for Ebay update
  35. // Version 4.0, release November 2010, add support for Top-rated seller filtering
  36. // Version 4.1, release November 2011, add @include to support changed eBay search links
  37. // Version 5.0, release March 2012, rework due to eBay changing internals
  38. // Version 6.0, release February 2013, rework due to eBay redesign
  39. // Version 7.0, release October 2014, update for eBay changes, newer Greasemonkey version compatibility
  40. // Version 8.0, release late July 2015, update for compatibility with searches within results
  41. // Version 8.1, release mid-August 2015, update to support searches with include description checked
  42. // Version 9.0, release March 2018, new Greasemonkey api compatibility
  43.  
  44. var defaultScoreThreshold = 50;
  45. var defaultPercentThreshold = 98.5;
  46. var defaultScoreMax = "";
  47. var FILTER_SCORE_FLAG = 1;
  48. var FILTER_PERCENT_FLAG = FILTER_SCORE_FLAG << 1;
  49. var FILTER_SCOREMAX_FLAG = FILTER_PERCENT_FLAG << 1;
  50. var filteredCount = 0;
  51. var buttonText = "button";
  52. var divText = "DIV";
  53. var spanText = "SPAN";
  54. var inputText = "INPUT";
  55. var imgText = "IMG";
  56. var brText = "BR";
  57. var ulText = "UL";
  58. var liText = "LI";
  59. var noneText = "none";
  60. var blockText = "block";
  61. var xText = "x";
  62. var changedText = "\u00a0 (Values changed.)";
  63. var changeNode = null;
  64. var configNode = null;
  65. var filteredNode = null;
  66. var scoreThreshold = defaultScoreThreshold;
  67. var percentThreshold = defaultPercentThreshold;
  68. var scoreMax = defaultScoreMax;
  69. var noTrsChecked = false;
  70.  
  71. var abefActiveControl = null;
  72.  
  73. async function getAbefConfiguration()
  74. {
  75. var abefParam = await GM.getValue("scoreThreshold", xText);
  76. if (abefParam != xText)
  77. {
  78. scoreThreshold = abefParam - 0;
  79. }
  80. else
  81. {
  82. GM.setValue("scoreThreshold", scoreThreshold + "");
  83. }
  84. abefParam = await GM.getValue("percentThreshold", xText);
  85. if (abefParam != xText)
  86. {
  87. percentThreshold = abefParam - 0;
  88. }
  89. else
  90. {
  91. GM.setValue("percentThreshold", percentThreshold + "");
  92. }
  93. abefParam = await GM.getValue("scoreMax", xText);
  94. if (abefParam != xText)
  95. {
  96. if (isNaN(parseInt(abefParam)))
  97. {
  98. scoreMax = "";
  99. }
  100. else
  101. {
  102. scoreMax = abefParam - 0;
  103. }
  104. }
  105. else
  106. {
  107. GM.setValue("scoreMax", scoreMax + "");
  108. }
  109.  
  110. abefParam = await GM.getValue("noTrsChecked", xText);
  111. if (abefParam != xText)
  112. {
  113. noTrsChecked = abefParam;
  114. }
  115. else
  116. {
  117. GM.setValue("noTrsChecked", noTrsChecked);
  118. }
  119. }
  120.  
  121. function updateAbefConfiguration()
  122. {
  123. GM.setValue("scoreThreshold", scoreThreshold + "");
  124. GM.setValue("percentThreshold", percentThreshold + "");
  125. GM.setValue("scoreMax", scoreMax + "");
  126. GM.setValue("noTrsChecked",noTrsChecked);
  127. }
  128.  
  129. function showFilteredCount()
  130. {
  131. filteredNode.nodeValue =
  132. "\u00a0 " + filteredCount + " entr"+ (filteredCount != 1 ? "ies" : "y") + " filtered from view";
  133. configNode.parentNode.appendChild(filteredNode);
  134. }
  135.  
  136. function newFilterValues(event)
  137. {
  138. if (event != null)
  139. {
  140. event.preventDefault();
  141. event.stopPropagation();
  142. }
  143. if (configNode.nextSibling == changeNode)
  144. {
  145. configNode.parentNode.removeChild(changeNode);
  146. }
  147.  
  148. var fNode = document.getElementById("fbScore");
  149. scoreThreshold = fNode.value - 0; // force numeric
  150. fNode = document.getElementById("fbPercent");
  151. percentThreshold = fNode.value - 0;
  152. fNode = document.getElementById("fbMax");
  153. if (isNaN(parseInt(fNode.value)))
  154. {
  155. scoreMax = "";
  156. }
  157. else
  158. {
  159. scoreMax = fNode.value - 0;
  160. }
  161. fNode = document.getElementById("fbTrs");
  162. noTrsChecked = fNode.checked;
  163. updateAbefConfiguration();
  164. walkTheListings();
  165. }
  166.  
  167. function changedFilter(event)
  168. {
  169. if (event != null)
  170. {
  171. event.preventDefault();
  172. event.stopPropagation();
  173. }
  174. if (configNode.nextSibling == changeNode)
  175. {
  176. return;
  177. }
  178. if (configNode.nextSibling == filteredNode)
  179. {
  180. configNode.parentNode.removeChild(filteredNode);
  181. }
  182. configNode.parentNode.appendChild(changeNode);
  183. }
  184.  
  185. function getFeedbackScore(textNode)
  186. {
  187. if (!textNode.nodeValue)
  188. {
  189. return xText;
  190. }
  191. var s = textNode.nodeValue;
  192. var result;
  193. if ((result = s.match(/([\.,0-9]+)/)) != null)
  194. {
  195. // remove comma and period
  196. result[1] = result[1].replace(/[\.,]/g, "");
  197. return result[1] - 0; // force numeric
  198. }
  199. return xText;
  200. }
  201.  
  202. function getFeedbackPercent(textNode)
  203. {
  204. if (!textNode.nodeValue)
  205. {
  206. return xText;
  207. }
  208. var s = textNode.nodeValue;
  209. var result;
  210. // globalize by accepting comma as a period
  211. if ((result = s.match(/([\.,0-9]+)%/)) != null)
  212. {
  213. // change comma, if any, to period
  214. result[1] = result[1].replace(/,/, ".");
  215. return result[1] - 0; // force numeric
  216. }
  217. return xText;
  218. }
  219.  
  220. function shouldFilterOut(spanNode)
  221. {
  222. var cNode = spanNode.firstChild;
  223. var validScore = false;
  224. var validPercent = false;
  225. var feedbackScore = 0;
  226. var feedbackPercent = 0;
  227.  
  228. var parsed;
  229. while (!validScore || !validPercent)
  230. {
  231. parsed = false;
  232. if (cNode && spanNode.nodeName.toUpperCase() === spanText && cNode.nodeName === "#text")
  233. {
  234. if (!validPercent)
  235. {
  236. if ((feedbackPercent = getFeedbackPercent(cNode)) != xText)
  237. {
  238. validPercent = true;
  239. parsed = true;
  240. }
  241. }
  242. if (!validScore && !parsed)
  243. {
  244. if ((feedbackScore = getFeedbackScore(cNode)) != xText)
  245. {
  246. validScore = true;
  247. }
  248. }
  249. }
  250. spanNode = spanNode.nextSibling;
  251. if (!spanNode)
  252. {
  253. break;
  254. }
  255. cNode = spanNode.firstChild;
  256. }
  257.  
  258. var retValue = 0;
  259. if (validScore && feedbackScore < scoreThreshold)
  260. {
  261. retValue |= FILTER_SCORE_FLAG;
  262. }
  263. if (validPercent && feedbackPercent < percentThreshold)
  264. {
  265. retValue |= FILTER_PERCENT_FLAG;
  266. }
  267. if (validScore && !isNaN(parseInt(scoreMax)) && feedbackScore > scoreMax)
  268. {
  269. retValue |= FILTER_SCOREMAX_FLAG;
  270. }
  271. if (retValue)
  272. {
  273. filteredCount++;
  274. }
  275.  
  276. return retValue;
  277. }
  278.  
  279. function walkTheListings()
  280. {
  281. filteredCount = 0;
  282.  
  283. var xpath = "//div[@id='ResultSetItems']//li[contains(@id, 'item') and contains(@class, 'lvresult')]";
  284.  
  285. var liNodes = document.evaluate(
  286. xpath,
  287. document,
  288. null,
  289. XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
  290. null
  291. );
  292.  
  293. var entryCounter = 0;
  294. for (var loopVar = 0; loopVar < liNodes.snapshotLength; loopVar++)
  295. {
  296. var liNode = liNodes.snapshotItem(loopVar);
  297. var detailNode = liNode.firstChild;
  298. while (detailNode && (!detailNode.className || detailNode.nodeName.toUpperCase() !== ulText || detailNode.className.indexOf('lvdetails') < 0))
  299. {
  300. detailNode = detailNode.nextSibling;
  301. }
  302. if (!detailNode)
  303. {
  304. continue;
  305. }
  306.  
  307. var subLiNode = detailNode.firstChild;
  308. var spanNode = null;
  309. var found = false;
  310. while (subLiNode)
  311. {
  312. spanNode = subLiNode.firstChild;
  313. while (spanNode)
  314. {
  315. if (spanNode.nodeName.toUpperCase() === spanText && spanNode.className === 'selrat')
  316. {
  317. found = true;
  318. break;
  319. }
  320. spanNode = spanNode.nextSibling;
  321. }
  322. if (!found)
  323. {
  324. subLiNode = subLiNode.nextSibling;
  325. }
  326. else
  327. {
  328. break;
  329. }
  330. }
  331. if (!spanNode)
  332. {
  333. continue;
  334. }
  335.  
  336. var topSellerFilter = false;
  337. if (noTrsChecked)
  338. {
  339. var checkLiNode = subLiNode.nextSibling;
  340. while (checkLiNode)
  341. {
  342. if (checkLiNode.nodeName.toUpperCase() === liText)
  343. {
  344. var checkNode = checkLiNode.firstChild;
  345. while (checkNode && checkNode.nodeName.toUpperCase() !== spanText)
  346. {
  347. checkNode = checkNode.nextSibling;
  348. }
  349. if (checkNode)
  350. {
  351. // got SPAN
  352. checkNode = checkNode.firstChild;
  353. }
  354. while (checkNode && (checkNode.nodeName.toUpperCase() !== imgText || checkNode.className.indexOf("iconETRS") < 0))
  355. {
  356. checkNode = checkNode.nextSibling;
  357. }
  358. if (checkNode)
  359. {
  360. // found top seller IMG
  361. topSellerFilter = true;
  362. filteredCount++;
  363. break;
  364. }
  365. }
  366. checkLiNode = checkLiNode.nextSibling;
  367. }
  368. }
  369.  
  370. if (topSellerFilter || shouldFilterOut(spanNode))
  371. {
  372. if (liNode.style.display != noneText)
  373. {
  374. liNode.style.display = noneText;
  375. }
  376. }
  377. else
  378. {
  379. liNode.style.display = blockText;
  380. }
  381. }
  382. showFilteredCount();
  383. }
  384.  
  385. function buildControls()
  386. {
  387. var xpath;
  388. var divNodes;
  389.  
  390. xpath = "//div[@id='RelatedSearchesDF']";
  391.  
  392. divNodes = document.evaluate(
  393. xpath,
  394. document,
  395. null,
  396. XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
  397. null
  398. );
  399.  
  400. var isRelatedSearch = true;
  401. if (divNodes.snapshotLength <= 0)
  402. {
  403. // return false;
  404. isRelatedSearch = false;
  405. xpath = "//div[@id='TopPanelDF']";
  406. divNodes = document.evaluate(
  407. xpath,
  408. document,
  409. null,
  410. XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
  411. null
  412. );
  413. if (divNodes.snapshotLength <= 0)
  414. {
  415. return false;
  416. }
  417. }
  418.  
  419. changeNode = document.createTextNode(changedText);
  420. filteredNode = document.createTextNode("\u00a0 0 entries filtered from view");
  421.  
  422. var divSibling;
  423.  
  424. divSibling = divNodes.snapshotItem(0);
  425.  
  426. var newDivNode = document.createElement(divText);
  427.  
  428. if (!isRelatedSearch)
  429. {
  430. newDivNode.appendChild(document.createElement(brText));
  431. }
  432.  
  433. var span1Node = document.createElement(spanText);
  434. span1Node.style.fontWeight = "bold";
  435. var input1Node = document.createElement(inputText);
  436. input1Node.type = "text";
  437. input1Node.size = 4;
  438. input1Node.style.textAlign = "right";
  439. input1Node.setAttribute("id", "fbScore");
  440. input1Node.defaultValue = scoreThreshold;
  441. input1Node.addEventListener('change', changedFilter, false);
  442. span1Node.appendChild(input1Node);
  443. span1Node.appendChild(document.createTextNode(" Minimum feedback score\u00a0\u00a0\u00a0\u00a0"));
  444.  
  445. newDivNode.appendChild(span1Node);
  446. var span2Node = document.createElement(spanText);
  447. span2Node.style.fontWeight = "bold";
  448. var input2Node = document.createElement(inputText);
  449. input2Node.type = "text";
  450. input2Node.size = 4;
  451. input2Node.style.textAlign = "right";
  452. input2Node.setAttribute("id", "fbPercent");
  453. input2Node.defaultValue = percentThreshold;
  454. input2Node.addEventListener('change', changedFilter, false);
  455. span2Node.appendChild(input2Node);
  456. span2Node.appendChild(document.createTextNode("% Minimum positive feedback\u00a0\u00a0\u00a0\u00a0"));
  457. newDivNode.appendChild(span2Node);
  458.  
  459. var spanMaxNode = document.createElement(spanText);
  460. spanMaxNode.style.fontWeight = "bold";
  461. var inputMaxNode = document.createElement(inputText);
  462. inputMaxNode.type = "text";
  463. inputMaxNode.size = 4;
  464. inputMaxNode.style.textAlign = "right";
  465. inputMaxNode.setAttribute("id", "fbMax");
  466. inputMaxNode.defaultValue = scoreMax;
  467. inputMaxNode.addEventListener('change', changedFilter, false);
  468. spanMaxNode.appendChild(inputMaxNode);
  469. spanMaxNode.appendChild(document.createTextNode(" Maximum feedback score (blank to disable)\u00a0\u00a0\u00a0\u00a0"));
  470. newDivNode.appendChild(spanMaxNode);
  471.  
  472. var spanTrsNode = document.createElement(spanText);
  473. spanTrsNode.style.fontWeight = "bold";
  474. var inputTrsNode = document.createElement(inputText);
  475. inputTrsNode.type = "checkbox";
  476. inputTrsNode.size = 4;
  477. inputTrsNode.style.textAlign = "right";
  478. inputTrsNode.setAttribute("id", "fbTrs");
  479. inputTrsNode.checked = noTrsChecked;
  480. inputTrsNode.defaultChecked = noTrsChecked;
  481. inputTrsNode.addEventListener('change', changedFilter, false);
  482. spanTrsNode.appendChild(inputTrsNode);
  483. spanTrsNode.appendChild(document.createTextNode(" No Top-rated Sellers"));
  484. newDivNode.appendChild(spanTrsNode);
  485.  
  486. // second line
  487. newDivNode.appendChild(document.createElement(brText));
  488. configNode = document.createElement(buttonText);
  489. configNode.appendChild(document.createTextNode("Update Filter"));
  490. configNode.addEventListener('click', newFilterValues, false);
  491. newDivNode.appendChild(configNode);
  492.  
  493. divSibling.parentNode.insertBefore(document.createElement(brText), divSibling.nextSibling);
  494. divSibling.parentNode.insertBefore(document.createElement(brText), divSibling.nextSibling);
  495.  
  496. divSibling.parentNode.insertBefore(newDivNode, divSibling.nextSibling);
  497.  
  498. return true;
  499. }
  500.  
  501. async function init()
  502. {
  503. await getAbefConfiguration();
  504. if (!buildControls())
  505. {
  506. return;
  507. }
  508. walkTheListings();
  509. }
  510.  
  511. function main()
  512. {
  513. if (!GM.setValue)
  514. {
  515. return;
  516. }
  517. init();
  518. }
  519.  
  520.  
  521. main();
  522.