/share/spice/quixey/quixey.js

http://github.com/duckduckgo/zeroclickinfo-spice · JavaScript · 352 lines · 253 code · 58 blank · 41 comment · 39 complexity · 57eb5658025aa21a979fdd601051dee8 MD5 · raw file

  1. (function(env) {
  2. var quixey_image_domain = "d1z22zla46lb9g.cloudfront.net";
  3. // spice callback function
  4. env.ddg_spice_quixey = function(api_result) {
  5. if (!(api_result && api_result.results && api_result.results.length)) {
  6. return Spice.failed('apps');
  7. }
  8. var qLower = api_result.q.toLowerCase();
  9. categories = [
  10. "action",
  11. "adventure",
  12. "arcade",
  13. "board",
  14. "business",
  15. "casino",
  16. // "calculator",
  17. "design",
  18. "developer tools",
  19. "dice",
  20. "education",
  21. "educational",
  22. "entertainment",
  23. "family",
  24. "finance",
  25. "fitness",
  26. "graphics and design",
  27. "graphics",
  28. "health and fitness",
  29. "health",
  30. "kids",
  31. "lifestyle",
  32. "map",
  33. "medical",
  34. "music",
  35. "navigation",
  36. "networking",
  37. "news",
  38. "photography",
  39. "productivity",
  40. "puzzle",
  41. "racing",
  42. "role playing",
  43. "simulation",
  44. "social networking",
  45. "social",
  46. "sports",
  47. "strategy",
  48. "travel",
  49. "trivia",
  50. "utilities",
  51. "video",
  52. "weather"
  53. ],
  54. // force startend to prevent categories
  55. // from doing silly stuff on queries like
  56. // 'yahoo news digest'
  57. category_alt_start = categories.join('|^'),
  58. category_alt_end = categories.join('$|'),
  59. category_trigger_regexp = new RegExp('^'+category_alt_start+'|'+category_alt_end+'$', 'i'),
  60. category_match_regexp = new RegExp(qLower.match(category_trigger_regexp), 'i');
  61. Spice.add({
  62. id: 'apps',
  63. name: 'Apps',
  64. data: api_result.results,
  65. meta: {
  66. // count is now computed by Spice
  67. // count: relevants.length,
  68. total: api_result.results.length,
  69. itemType: 'Apps',
  70. sourceName: 'Quixey',
  71. sourceUrl: 'https://www.quixey.com/search?q=' + encodeURIComponent(qLower),
  72. sourceLogo: {
  73. url: DDG.get_asset_path('quixey','quixey_logo.png'),
  74. width: '45',
  75. height: '12'
  76. }
  77. },
  78. normalize : function(item) {
  79. // relevancy pre-filter: skip ones that have less than three reviews
  80. if (item.rating_count && item.rating_count < 3) {
  81. return null;
  82. }
  83. // relevancy pre-filter: remove GoogleTV Android packages
  84. // API doesn't differentiate between GoogleTV Android and Android packages
  85. var reFilter = /Google\s?TV/i;
  86. for (var i in item.editions) {
  87. var packageAppName = [DDG.getProperty(item, "editions." + [i] + ".custom.features.package_name"),
  88. DDG.getProperty(item, "editions." + [i] + ".name")].join(" ");
  89. if (packageAppName.match(reFilter)) {
  90. item.editions.splice(i,1);
  91. }
  92. }
  93. // ignoring the case where rating_count is null
  94. var icon_url = make_icon_url(item), screenshot;
  95. if (!icon_url)
  96. return null;
  97. screenshot = DDG.getProperty(item, 'editions.0.screenshots.0.image_url');
  98. if (!screenshot)
  99. return null;
  100. if (item.name && item.name.toLowerCase() === qLower) {
  101. item.exactMatch = true;
  102. } else if (item.developer && item.developer.name && item.developer.name.toLowerCase() === qLower) {
  103. item.boost = true;
  104. }
  105. return {
  106. 'img': DDG.toHTTPS(icon_url),
  107. 'title': item.name,
  108. 'heading': item.name,
  109. 'rating': item.rating,
  110. 'reviewCount': DDG.getProperty(item, "editions.0.custom.features.allversions_rating_count") || DDG.getProperty(item, "editions.0.custom.features.rating_count"),
  111. 'url_review': item.dir_url,
  112. 'price': pricerange(item),
  113. 'abstract': item.short_desc || "",
  114. 'brand': (item.developer && item.developer.name) || "",
  115. 'products_buy': Spice.quixey.quixey_buy,
  116. // this should be the array of screenshots with captions
  117. // and check for the existence of them
  118. 'img_m': quixey_image(screenshot)
  119. };
  120. },
  121. relevancy: {
  122. type: qLower.match(category_trigger_regexp) ? "category" : "primary",
  123. skip_words: [
  124. "android",
  125. "app",
  126. "apple app store",
  127. "apple app",
  128. "application",
  129. "applications",
  130. "apps",
  131. "best",
  132. "blackberry",
  133. "download",
  134. "downloaded",
  135. "droid",
  136. "enhanced",
  137. "free",
  138. "fully featured",
  139. "google play store",
  140. "google play",
  141. "ios",
  142. "ipad",
  143. "iphone",
  144. "ipod touch",
  145. "ipod",
  146. "most",
  147. "playbook",
  148. "popular",
  149. "release data",
  150. "release",
  151. "sale",
  152. "search",
  153. "windows mobile",
  154. "windows phone 8",
  155. "windows phone"
  156. ],
  157. category: [
  158. { required: 'icon_url' },
  159. { key: 'short_desc' },
  160. { key: 'name' },
  161. { key: 'custom.features.category', match: category_match_regexp, strict:false } // strict means this key has to contain a category phrase or we reject
  162. ],
  163. primary: [
  164. { required: 'icon_url' }, // would like to add alt: 'platforms.0.icon_url'
  165. { key: 'name', strict: false },
  166. ],
  167. // field for de-duplication
  168. // currently single value, could be differentiated by type
  169. // eg { 'category': 'name', 'primary': 'id' }
  170. dup: 'name'
  171. },
  172. // sort field definition: how this data can be sorted. each field may appear in a UI drop-down.
  173. sort_fields: {
  174. 'rating': //Spice.rating_comparator('rating', 'rating_count')
  175. function(a,b) {
  176. var rank = function(r,count) { // will be moved up
  177. if (!count) count = 1; // rating_count is sometimes null
  178. return r*r*r*count/5;
  179. };
  180. // using normalized values
  181. return rank(a.rating,a.reviewCount ) > rank(b.rating,b.reviewCount ) ? -1: 1;
  182. }
  183. // 'platform': function(a,b) { } ,
  184. // 'name':
  185. // 'price':
  186. },
  187. // default sorting- how the data will be initially arranged.
  188. // depends on the type of search. here only a 'category' search will
  189. // perform a default initial sort.
  190. // 'primary' is not here, meaning will remain in api_result order.
  191. // sort_default: { <sorttype>:<field>, <sorttype>:<field>, ... }
  192. sort_default: { 'category': 'rating' },
  193. templates: {
  194. group: 'products',
  195. options: {
  196. buy: Spice.quixey.buy
  197. },
  198. variants: {
  199. tile: 'narrow'
  200. }
  201. }
  202. });
  203. };
  204. // format a price
  205. // p is expected to be a number
  206. function qprice(p) {
  207. "use strict";
  208. if (p == 0) { // == type coercion is ok here
  209. return "FREE";
  210. }
  211. return "$" + (p/100).toFixed(2).toString();
  212. }
  213. // template helper for price formatting
  214. // {{price x}}
  215. Handlebars.registerHelper("Quixey_qprice", function(obj) {
  216. "use strict";
  217. return qprice(obj);
  218. });
  219. var quixey_image = function(image_url) {
  220. return "http://" + quixey_image_domain + image_url.match(/\/image\/.+/)[0];
  221. }
  222. var make_icon_url = function(item) {
  223. var domain = quixey_image_domain,
  224. icon_url = item.icon_url;
  225. if (!icon_url) {
  226. // console.warn("quixey: icon_url is null for %o", item);
  227. if (item.editions && item.editions[0].icon_url)
  228. icon_url = item.editions[0].icon_url;
  229. else
  230. return null;
  231. }
  232. // Get the image server that the icon_url in platforms is pointing to.
  233. // It's not ideal, but the link to the app's image still has to redirect
  234. // and it redirects to HTTPS. What we want is an HTTP link (for speed).
  235. if (item.platforms && item.platforms.length > 0 && item.platforms[0].icon_url) {
  236. domain = item.platforms[0].icon_url.match(/https?:\/\/([^\/]+)/)[1];
  237. }
  238. // Replace the domain in our icon_url to the one that we got from
  239. // the platforms array.
  240. // return "/iu/?u=http://" + domain + item.icon_url.match(/\/image\/.+/)[0] + "&f=1";
  241. return "http://" + domain + icon_url.match(/\/image\/.+/)[0];
  242. };
  243. // template helper to format a price range
  244. var pricerange = function(item) {
  245. if (!item || !item.editions)
  246. return "";
  247. var low = item.editions[0].cents;
  248. var high = item.editions[0].cents;
  249. var tmp, range, lowp, highp;
  250. for (var i in item.editions) {
  251. tmp = item.editions[i].cents;
  252. if (tmp < low) low = tmp;
  253. if (tmp > high) high = tmp;
  254. }
  255. lowp = qprice(low);
  256. if (high > low) {
  257. highp = qprice(high);
  258. range = lowp + " - " + highp;
  259. item.hasPricerange = true;
  260. } else {
  261. range = lowp;
  262. }
  263. return range;
  264. };
  265. // template helper to replace iphone and ipod icons with
  266. // smaller 'Apple' icons
  267. Handlebars.registerHelper("Quixey_platform_icon", function(icon_url) {
  268. "use strict";
  269. if (this.id === 2004 || this.id === 2015) {
  270. return "https://icons.duckduckgo.com/i/itunes.apple.com.ico";
  271. }
  272. return "/iu/?u=" + icon_url + "&f=1";
  273. });
  274. // template helper that returns and unifies platform names
  275. Handlebars.registerHelper("Quixey_platform_name", function() {
  276. "use strict";
  277. var name;
  278. var platforms = this.platforms;
  279. name = platforms[0].name;
  280. if (platforms.length > 1) {
  281. switch (platforms[0].name) {
  282. case "iPhone" :
  283. case "iPad" :
  284. name = "iOS";
  285. break;
  286. case "Blackberry":
  287. case "Blackberry 10":
  288. name = "Blackberry";
  289. break;
  290. }
  291. }
  292. return name;
  293. });
  294. })(this);