ODInfo

Parses OD listings.

Script này sẽ không được không được cài đặt trực tiếp. Nó là một thư viện cho các script khác để bao gồm các chỉ thị meta // @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);