Greasy Fork is available in English.

ODInfo

Parses OD listings.

بۇ قوليازمىنى بىۋاسىتە قاچىلاشقا بولمايدۇ. بۇ باشقا قوليازمىلارنىڭ ئىشلىتىشى ئۈچۈن تەمىنلەنگەن ئامبار بولۇپ، ئىشلىتىش ئۈچۈن مېتا كۆرسەتمىسىگە قىستۇرىدىغان كود: // @require https://update.greatest.deepsurf.us/scripts/7055/28946/ODInfo.js

  1. // ==UserScript==
  2. // @name ODInfo
  3. // @namespace varb
  4. // @version 0.3
  5. // @description Parses OD listings.
  6. // @license WTFPL Version 2; http:www.wtfpl.net/txt/copying/
  7. // ==/UserScript==
  8.  
  9. // @param {string|Document} source - url to an OD listing or a Document object of one
  10. // @param {object[]} tasks - single 'attribute' or 'private' key disables the given task,
  11. // otherwise one is extended or appended to the chain
  12. // @returns {object} Q promise which fulfills with an object of parsed 'attributes'
  13. var OD_getInfo = (function ($) {
  14.  
  15. 'use strict';
  16.  
  17. var ctx = {
  18. tasks: [
  19. {
  20. attribute: 'odid',
  21. get: function () {
  22. return this.$doc.find('meta[property="od:id"]').attr('content')
  23. || this.rawdoc.match(/meta.+?od:id.+?content=\"([-\w]+)\"/i)[1];
  24. },
  25. test: function (odid) {
  26. return typeof odid === 'string' && odid.length;
  27. }
  28. },
  29. {
  30. attribute: 'authors',
  31. get: function () {
  32. var authors = this.$meta.find('span:contains(Creators)').next().find('dd');
  33. return authors.length
  34. ? authors.list()
  35. : this.$doc.find('.creator a[href^="/creators/"]').list();
  36. },
  37. test: function (authors) {
  38. return Array.isArray(authors) && authors.length;
  39. }
  40. },
  41. {
  42. attribute: 'title',
  43. get: function () {
  44. var suffix = [null, 'st', 'nd', 'rd'],
  45. title, subtitle, ed, series;
  46. subtitle = this.$doc.find('.pageHeader .subtitle').contents()[0];
  47. subtitle = subtitle && subtitle.textContent.trim();
  48. ed = this.$meta.find('dt:contains(Edition)').next().text();
  49. if (ed) {
  50. ed += (ed.search(/([^1]|\b)[123]$/) !== -1) ? suffix[ed.substr(-1)] : 'th';
  51. }
  52. series = this.$meta.find('dt:contains(Series)').next().text().trim();
  53. return this.$doc.find('h1.title').text().trim()
  54. + (subtitle ? ': ' + subtitle : '')
  55. + (series ? ' (' + series + ')' : '')
  56. + (ed ? ' (' + ed + ' Edition)' : '');
  57. },
  58. test: function (title) {
  59. return typeof title === 'string' && title.length;
  60. }
  61. },
  62. {
  63. attribute: 'publisher',
  64. get: function () {
  65. return this.$meta.find('dt:contains(Publisher)').next().text().trim();
  66. },
  67. test: function (publisher) {
  68. return typeof publisher === 'string' && publisher.length;
  69. }
  70. },
  71. {
  72. attribute: 'pubyear',
  73. get: function () {
  74. return +this.$meta.find('dt:contains(Publication Date)').next().text();
  75. },
  76. test: function (pubyear) {
  77. return typeof pubyear === 'number' && pubyear;
  78. }
  79. },
  80. {
  81. attribute: 'formats',
  82. get: function () {
  83. var formats = this.$meta.find('span:contains(Format)').next().text().toLowerCase();
  84. return formats.match(/(\w+)(?=\s(audio|e)?book)/g).filter(function (f, i, a) {
  85. return i === 0 || a.slice(0, i).indexOf(f) === -1;
  86. });
  87. },
  88. test: function (formats) {
  89. return Array.isArray(formats) && formats.length;
  90. }
  91. },
  92. {
  93. // prepare access to supplemental data
  94. private: 'suppresp',
  95. get: function () {
  96. var that = this;
  97.  
  98. return Q.Promise(function (ok, fail) {
  99. var requests = [];
  100.  
  101. that.attrs.formats.forEach(function (format) {
  102. requests.push(suppRequest(that.attrs.odid, format));
  103. });
  104.  
  105. Q.allSettled(requests).then(function (results) {
  106. var suppresp = {};
  107. results.forEach(function (result) {
  108. if (result.state === 'fulfilled') {
  109. suppresp[result.value.format] = $(result.value.page);
  110. } else {
  111. console.error('odinfo: Supplemental request: ' + result.reason);
  112. }
  113. });
  114. return suppresp;
  115. }).then(function (suppresp) {
  116. ok(suppresp);
  117. }).catch(function (err) {
  118. fail(err);
  119. });
  120. });
  121. },
  122. test: function (suppresp) {
  123. return typeof suppresp === 'object' && this.attrs.formats.every(function (f) {
  124. return typeof suppresp[f] === 'object';
  125. });
  126. }
  127. },
  128. {
  129. attribute: 'isbnbyformat',
  130. get: function () {
  131. var format, $page, ia, result = {};
  132. for (format in this.suppresp) {
  133. $page = this.suppresp[format];
  134. ia = $page.find('td:contains(ISBN):last').next().text().trim()
  135. || $page.find('td:contains(ASIN)').next().text().trim();
  136. if (ia) {
  137. result[format] = ia;
  138. } else {
  139. console.error('odinfo: failed to extract isbn or asin.');
  140. }
  141. }
  142. return result;
  143. },
  144. test: function (isbnbyformat) {
  145. return typeof isbnbyformat === 'object' && Object.keys(this.suppresp).some(function (f) {
  146. return typeof isbnbyformat[f] === 'string' && isbnbyformat[f].length;
  147. });
  148. }
  149. },
  150. {
  151. attribute: 'tags',
  152. get: function () {
  153. return this.$doc.find('.tags a').list();
  154. },
  155. test: function (tags) {
  156. return Array.isArray(tags) && tags.length;
  157. }
  158. },
  159. {
  160. attribute: 'coverurl',
  161. get: function () {
  162. var f, $page;
  163. for (f in this.suppresp) {
  164. $page = this.suppresp[f];
  165. try {
  166. return $page.find('.nav-link-admin').attr('href').match(/\('(http.+)'\);$/)[1];
  167. } catch (err) {
  168. console.error(err);
  169. }
  170. }
  171. },
  172. test: function (coverurl) {
  173. return typeof coverurl === 'string' && coverurl.length;
  174. }
  175. },
  176. {
  177. attribute: 'description',
  178. get: function () {
  179. var $desc = this.$doc.find('.description').clone();
  180. $desc.children().remove('.tags, .meta').andSelf()
  181. .find('span:contains(»)').next().andSelf().remove();
  182. return $desc.toBBCode().trim();
  183. },
  184. test: function (desc) {
  185. return typeof desc === 'string' && desc.length;
  186. }
  187. }]
  188. };
  189.  
  190. ctx.indexFor = ctx.tasks.reduce(function (o, t, i) {
  191. o[t.attribute || t.private] = i;
  192. return o;
  193. }, {});
  194.  
  195. return function (args) {
  196. extendTasks(args.tasks || []);
  197.  
  198. return fetch(args.source)
  199. .then(parse);
  200. };
  201.  
  202. function extendTasks(tk) {
  203. tk.forEach(function (task) {
  204. var i = ctx.indexFor[task.attribute || task.private];
  205. if (!i) {
  206. ctx.tasks.push(task);
  207. } else if (Object.keys(task).length === 1) {
  208. ctx.tasks.splice(i, 1);
  209. } else {
  210. $.extend(ctx.tasks[i], task);
  211. }
  212. });
  213. }
  214.  
  215. function suppRequest(odid, format) {
  216. var codeFor = {wma: 25, mp3: 425, epub: 410, pdf: 50, kindle: 420};
  217. return Q.Promise(function (ok, fail) {
  218. GM_xmlhttpRequest({
  219. method: 'GET',
  220. url: 'https://www.contentreserve.com/TitleInfo.asp?ID={' + odid + '}&Format=' + codeFor[format],
  221. onload: function (res) {
  222. ok({page: res.responseText, format: format});
  223. },
  224. onerror: function (res) {
  225. fail('Failed requesting ia: ' + res.status + ' ' + res.statusText);
  226. }
  227. });
  228. });
  229. }
  230.  
  231. function fetch(from) {
  232. return Q.Promise(function (ok, fail) {
  233. if (typeof from === 'object' && from.nodeType && from.nodeType === 9) {
  234. ok(from);
  235. } else if (typeof from === 'string') {
  236. GM_xmlhttpRequest({
  237. url: from,
  238. method: 'GET',
  239. onload: function (res) {
  240. ok(res.responseText);
  241. },
  242. onerror: function (res) {
  243. fail(new Error('Failed fetching od listing: ' + res.status + ' ' + res.statusText));
  244. }
  245. });
  246. } else {
  247. fail(new TypeError('Invalid source. URL or Document expected.'));
  248. }
  249. });
  250. }
  251.  
  252. function TaskError(message, task) {
  253. this.name = 'TaskError';
  254. this.message = message;
  255. this.taskName = task.attribute || task.private;
  256. this.returned = task.returned;
  257. }
  258. TaskError.prototype = new Error();
  259. TaskError.prototype.constructor = TaskError;
  260.  
  261. // task objects are used for retrieving specific bits of information from a given listing.
  262. // the 'get' methods are provided a context with access to the raw and jQuery-wrapped Document.
  263. // the context can be extended directly with 'private' getters; 'attribute' values are grouped
  264. // in an object to be fulfilled by the resulting promise.
  265. // 'test' methods are not required; returning false, depending on the value of an optional
  266. // 'required' boolean, will either terminate the chain or log and carry on.
  267. function parse(doc) {
  268. return Q.Promise(function (ok, fail) {
  269. var $doc = $(doc),
  270. p = Q(), attr;
  271.  
  272. $.extend(ctx, {
  273. attrs: {},
  274. rawdoc: doc,
  275. $doc: $doc,
  276. $meta: $doc.find('.meta-accordion')
  277. });
  278.  
  279. ctx.tasks.forEach(function (task) {
  280. p = p.then(function () {
  281. return task.get.call(ctx);
  282. }).then(function (val) {
  283. var passed = task.test ? task.test.call(ctx, val) : true;
  284. if (passed) {
  285. if (task.attribute) {
  286. ctx.attrs[task.attribute] = val;
  287. } else if (task.private) {
  288. ctx[task.private] = val;
  289. }
  290. } else {
  291. throw new TaskError('Test failed.', $.extend({}, task, {returned: val}));
  292. }
  293. }).catch(function (err) {
  294. var tn = err.taskName || task.attribute || task.private,
  295. m = 'odinfo: %s in %s: %s',
  296. errargs = [m, err.name, tn, err.message];
  297.  
  298. if (task.required) {
  299. fail(err instanceof TaskError ? err : new TaskError(err.message, task));
  300. } else {
  301. if (err.returned !== undefined) {
  302. errargs[0] += ' Task returned %o';
  303. errargs.push(err.returned);
  304. }
  305. console.error.apply(console, errargs);
  306. }
  307. });
  308. });
  309.  
  310. p.then(function () {
  311. if (Object.keys(ctx.attrs).length) {
  312. ok(ctx.attrs);
  313. } else {
  314. fail(new Error('Failed to parse listing'));
  315. }
  316. });
  317. });
  318. }
  319.  
  320. })(jQuery);
  321.  
  322.  
  323. (function ($) {
  324.  
  325. // retrieve an array of text contents from the set of matched elements
  326. $.fn.list = function () {
  327. return $.map(this, function (e) {
  328. return e.textContent;
  329. });
  330. };
  331.  
  332. // flatten a set of matched elements wrapping content in bbcode entities
  333. $.fn.toBBCode = function () {
  334. var nodes = this.get(),
  335. frag = document.createDocumentFragment();
  336. nodes.forEach(function (node) {
  337. frag.appendChild(node.cloneNode(true));
  338. });
  339. return strip(frag);
  340. };
  341.  
  342. var entityFor = {
  343. P: function (s) { return '\n' + s + '\n'; },
  344. BR: function () { return '\n'; },
  345. I: function (s) { return '[i]' + s + '[/i]'; },
  346. EM: function (s) { return this.I(s); },
  347. B: function (s) { return '[b]' + s + '[/b]'; },
  348. STRONG: function (s) { return this.B(s); },
  349. UL: function (s) { return '\n[ul]' + s + '[/ul]\n'; },
  350. OL: function (s) { return '\n[ol]' + s + '[/ol]\n'; },
  351. LI: function (s) { return '[*]' + s + '\n'; },
  352. A: function (s, n) { return '[url=' + n.href + ']' + s + '[/url]'; }
  353. };
  354.  
  355. function strip(node) {
  356. var result = '',
  357. parent = node;
  358. if (node.nodeType === 3) {
  359. return node.textContent;
  360. }
  361. if (node.nodeType !== 1 && node.nodeType !== 11) {
  362. return '';
  363. }
  364. node = node.firstChild;
  365. while (node) {
  366. result += strip(node);
  367. node = node.nextSibling;
  368. }
  369. return entityFor[parent.nodeName] ? entityFor[parent.nodeName](result, parent) : result;
  370. }
  371.  
  372. })(jQuery);