Manga Loader

Loads manga chapter into one page in a long strip format, supports switching chapters and works for a variety of sites, minimal script with no dependencies, easy to implement new sites

As of 2014-11-07. See the latest version.

  1. // ==UserScript==
  2. // @name Manga Loader
  3. // @namespace http://www.fuzetsu.com/MangaLoader
  4. // @version 1.3.6
  5. // @description Loads manga chapter into one page in a long strip format, supports switching chapters and works for a variety of sites, minimal script with no dependencies, easy to implement new sites
  6. // @copyright 2014+, fuzetsu
  7. // @grant GM_getValue
  8. // @grant GM_setValue
  9. // @grant GM_deleteValue
  10. // @grant GM_registerMenuCommand
  11. // @match http://bato.to/read/*
  12. // @match http://mangafox.me/manga/*/*/*
  13. // @match http://readms.com/r/*/*/*/*
  14. // @match http://g.e-hentai.org/s/*/*
  15. // @match http://exhentai.org/s/*/*
  16. // @match *://www.fakku.net/*/*/read*
  17. // @match http://www.mangareader.net/*/*
  18. // @match http://www.mangahere.co/manga/*/*
  19. // @match http://www.mangapanda.com/*/*
  20. // @match http://mangadeer.com/manga/*/*/*/*
  21. // @match http://mngacow.com/*/*
  22. // @match http://nowshelf.com/watch/*
  23. // @match http://nhentai.net/g/*/*
  24. // @match http://centraldemangas.net/online/*/*
  25. // @match http://www.mangatown.com/manga/*/*/*
  26. // ==/UserScript==
  27.  
  28. // set to true for manga load without prompt
  29. var BM_MODE = false;
  30.  
  31. var scriptName = 'Manga Loader';
  32.  
  33. /**
  34. Sample Implementation:
  35. {
  36. match: "http://domain.com/.*" // the url to react to for manga loading
  37. , img: '#image' // css selector to get the page's manga image
  38. , next: '#next_page' // css selector to get the link to the next page
  39. , numpages: '#page_select' // css selector to get the number of pages. elements like (select, span, etc)
  40. , curpage: '#page_select' // css selector to get the current page. usually the same as numPages if it's a select element
  41. , nextchap: '#next_chap' // css selector to get the link to the next chapter
  42. , prevchap: '#prev_chap' // same as above except for previous
  43. , wait: 3000 // how many ms to wait before auto loading (to wait for elements to load)
  44. , pages: function(next_url, current_page_number, callback, extract_function) {
  45. // gets called requesting a certain page number (current_page_number)
  46. // to continue loading execute callback with img to append as first parameter and next url as second parameter
  47. // only really needs to be used on sites that have really unusual ways of loading images or depend on javascript
  48. }
  49.  
  50. Any of the CSS selectors can be functions instead that return the desired value.
  51. }
  52. */
  53.  
  54. var implementations = [
  55. { // Batoto
  56. match: "^http://bato.to/read/.*"
  57. , img: '#comic_page'
  58. , next: '#full_image + div > a'
  59. , numpages: '#page_select'
  60. , curpage: '#page_select'
  61. , nextchap: 'select[name=chapter_select]'
  62. , prevchap: 'select[name=chapter_select]'
  63. , invchap: true
  64. }
  65. , { // MangaPanda
  66. match: "^http://www.mangapanda.com/.*/[0-9]*"
  67. , img: '#img'
  68. , next: '.next a'
  69. , numpages: '#pageMenu'
  70. , curpage: '#pageMenu'
  71. , nextchap: 'td.c5 + td a'
  72. , prevchap: 'table.c6 tr:last-child td:last-child a'
  73. }
  74. , { // MangaFox
  75. match: "^http://mangafox.me/manga/.*/.*/.*"
  76. , img: '#image'
  77. , next: '.next_page'
  78. , numpages: 'select.m'
  79. , curpage: 'select.m'
  80. , nextchap: '#chnav p + p a'
  81. , prevchap: '#chnav a'
  82. }
  83. , { // MangaStream
  84. match: "^http://readms.com/r/.*/.*/.*"
  85. , img: '#manga-page'
  86. , next: '.next a'
  87. , numpages: function() {
  88. var lastPage = getEl('.subnav-wrapper .controls .btn-group:last-child ul li:last-child');
  89. return parseInt(lastPage.textContent.match(/[0-9]/g).join(''), 10);
  90. }
  91. , nextchap: function(prev) {
  92. var found;
  93. var chapters = [].slice.call(document.querySelectorAll('.controls > div:first-child > .dropdown-menu > li a'));
  94. chapters.pop();
  95. for(var i = 0; i < chapters.length; i++) {
  96. if(window.location.href.indexOf(chapters[i].href) !== -1) {
  97. found = chapters[i + (prev ? 1 : -1)];
  98. if(found) return found.href;
  99. }
  100. }
  101. }
  102. , prevchap: function() {
  103. return this.nextchap(true);
  104. }
  105. }
  106. , { // MangaReader
  107. match: "^http://www.mangareader.net/.*/.*"
  108. , img:'#img'
  109. , next: '.next a'
  110. , numpages: '#pageMenu'
  111. , curpage: '#pageMenu'
  112. , nextchap: 'td.c5 + td a'
  113. , prevchap: 'table.c6 tr:last-child td:last-child a'
  114. }
  115. , { // MangaTown
  116. match: "^http://www.mangatown.com/manga/[^/]*/v[0-9]*/c[0-9]*"
  117. , img:'#image'
  118. , next: '#viewer a'
  119. , numpages: '.page_select select'
  120. , curpage: '.page_select select'
  121. , nextchap: '#top_chapter_list'
  122. , prevchap: '#top_chapter_list'
  123. , wait: 1000
  124. }
  125. , { // MangaCow
  126. match: "^http://mngacow\.com/.*/[0-9]*"
  127. , img: '.prw > a > img'
  128. , next: '.prw > a:last-child'
  129. , numpages: 'select.cbo_wpm_pag'
  130. , curpage: 'select.cbo_wpm_pag'
  131. , nextchap: function(prev) {
  132. var chapSel = getEl('select.cbo_wpm_chp');
  133. var nextChap = chapSel.options[chapSel.selectedIndex + (prev ? 1 : -1)];
  134. if(nextChap) {
  135. return 'http://mngacow.com/' + window.location.pathname.slice(1, window.location.pathname.slice(1).indexOf('/') + 2) + nextChap.value;
  136. }
  137. }
  138. , prevchap: function() {
  139. return this.nextchap(true);
  140. }
  141. }
  142. , { // MangaHere
  143. match: "^http://www.mangahere.co/manga/.*/.*"
  144. , img: '#viewer img'
  145. , next: '#viewer a'
  146. , numpages: 'select.wid60'
  147. , curpage: 'select.wid60'
  148. , nextchap: '.reader_tip > p:nth-last-child(2) > a'
  149. , prevchap: '.reader_tip > p:nth-last-child(1) > a'
  150. }
  151. , { // MangaDeer
  152. match: "^http://mangadeer\.com/manga/.*"
  153. , img: '.img-link > img'
  154. , next: '.page > span:last-child > a'
  155. , numpages: '#sel_page_1'
  156. , curpage: '#sel_page_1'
  157. , nextchap: function(prev) {
  158. var ddl = getEl('#sel_book_1');
  159. var index = ddl.selectedIndex + (prev ? -1 : 1);
  160. if(index >= ddl.options.length) return;
  161. var mangaName = window.location.href.slice(window.location.href.indexOf('manga/') + 6);
  162. mangaName = mangaName.slice(0, mangaName.indexOf('/'));
  163. return 'http://mangadeer.com/manga/' + mangaName + ddl.options[index].value + '/1';
  164. }
  165. , prevchap: function() {
  166. return this.nextchap(true);
  167. }
  168. }
  169. , { // Central de Mangas
  170. match: "^http://centraldemangas.net/online/[^\/]*/[0-9]*"
  171. , img: '#manga-page'
  172. , next: '#manga-page'
  173. , numpages: '#manga_pages'
  174. , curpage: '#manga_pages'
  175. , nextchap: function(prev) {
  176. var url = window.location.href
  177. , chapters = getEl('#manga_caps')
  178. , urlPre = url.slice(0, url.lastIndexOf('/') + 1)
  179. , newChap = chapters.options[chapters.selectedIndex + (prev ? -1 : 1)];
  180. return newChap ? urlPre + newChap.textContent : null;
  181. }
  182. , prevchap: function() {
  183. return this.nextchap(true);
  184. }
  185. , pages: function(url, num, cb, ex) {
  186. var url = url.slice(0, url.lastIndexOf('-') + 1) + ("0" + num).slice(-2) + url.slice(url.lastIndexOf('.'));
  187. cb(url, url);
  188. }
  189. }
  190. , { // GEH/EXH
  191. match: "^http://(g.e-hentai|exhentai).org/s/.*/.*"
  192. , img: '.sni > a > img, #img'
  193. , next: '.sni > a, #i3 a'
  194. }
  195. , { // Fakku
  196. match: "^http(s)?://www.fakku.net/.*/.*/read"
  197. , img: '.current-page'
  198. , next: '.current-page'
  199. , numpages: '.drop'
  200. , curpage: '.drop'
  201. , pages: function(url, num, cb, ex) {
  202. var firstNum = url.lastIndexOf('/')
  203. , lastDot = url.lastIndexOf('.');
  204. var c = url.charAt(firstNum);
  205. while(c && !/[0-9]/.test(c)) {
  206. c = url.charAt(++firstNum);
  207. }
  208. var curPage = parseInt(url.slice(firstNum, lastDot), 10);
  209. var url = url.slice(0, firstNum) + ('00' + (curPage + 1)).slice(-3) + url.slice(lastDot);
  210. cb(url, url);
  211. }
  212. }
  213. , { // Nowshelf
  214. match: "^http://nowshelf.com/watch/[0-9]*"
  215. , img: '#image'
  216. , next: '#image'
  217. , numpages: function() {return parseInt(getEl('#page').textContent.slice(3), 10);}
  218. , curpage: function() {return parseInt(getEl('#page > input').value, 10);}
  219. , pages: function(url, num, cb, ex) {
  220. var url = url.slice(0, -7) + ('00' + num).slice(-3) + url.slice(-4);
  221. cb(url, url);
  222. }
  223. }
  224. , { // nhentai
  225. match: "^http://nhentai\.net\/g\/[0-9]*/[0-9]*"
  226. , img: '#image-container > a > img'
  227. , next: '#image-container > a'
  228. , numpages: '.num-pages'
  229. , curpage: '.current'
  230. , imgmod: {altProp: 'data-cfsrc'}
  231. }
  232. ];
  233.  
  234. var log = function(msg, type) {
  235. type = type || 'log';
  236. if(type === 'exit') {
  237. throw scriptName + ' exit: ' + msg;
  238. } else {
  239. console[type](scriptName + ' ' + type + ': ', msg);
  240. }
  241. };
  242.  
  243. var getEl = function(q, c) {
  244. if(!q) return;
  245. return (c || document).querySelector(q);
  246. };
  247.  
  248. var storeGet = function(key) {
  249. if(typeof GM_getValue === "undefined") {
  250. var value = localStorage.getItem(key);
  251. if(value === "true" || value === "false") {
  252. return (value === "true") ? true : false;
  253. }
  254. return value;
  255. }
  256. return GM_getValue(key);
  257. };
  258.  
  259. var storeSet = function(key, value) {
  260. if(typeof GM_setValue === "undefined") {
  261. return localStorage.setItem(key, value);
  262. }
  263. return GM_setValue(key, value);
  264. };
  265.  
  266. var storeDel = function(key) {
  267. if(typeof GM_deleteValue === "undefined") {
  268. return localStorage.removeItem(key);
  269. }
  270. return GM_deleteValue(key);
  271. };
  272.  
  273. var extractInfo = function(selector, mod, context) {
  274. selector = this[selector];
  275. if(typeof selector === 'function') {
  276. return selector.call(this);
  277. }
  278. var elem = getEl(selector, context)
  279. , option;
  280. mod = mod || {};
  281. if(elem) {
  282. switch (elem.nodeName.toLowerCase()) {
  283. case 'img':
  284. return (mod.altProp && elem.getAttribute(mod.altProp)) || elem.getAttribute('src');
  285. case 'a':
  286. return elem.getAttribute('href');
  287. case 'ul':
  288. return elem.children.length;
  289. case 'select':
  290. switch(mod.type) {
  291. case 'index':
  292. return elem.options.selectedIndex + 1;
  293. case 'value':
  294. option = elem.options[elem.options.selectedIndex + (mod.val || 0)] || {};
  295. return option.value;
  296. default:
  297. return elem.options.length;
  298. }
  299. break;
  300. default:
  301. return elem.textContent;
  302. }
  303. }
  304. };
  305.  
  306. var toStyleStr = function(obj) {
  307. var stack = []
  308. , key;
  309. for(key in obj) {
  310. if(obj.hasOwnProperty(key)) {
  311. stack.push(key + ':' + obj[key]);
  312. }
  313. }
  314. return stack.join(';');
  315. };
  316.  
  317. var throttle = function(callback, limit) {
  318. var wait = false;
  319. return function() {
  320. if(!wait) {
  321. callback();
  322. wait = true;
  323. setTimeout(function() {
  324. wait = false;
  325. }, limit);
  326. }
  327. };
  328. };
  329.  
  330. var createButton = function(text, action, styleStr) {
  331. var button = document.createElement('button');
  332. button.textContent = text;
  333. button.onclick = action;
  334. button.setAttribute('style', styleStr || '');
  335. return button;
  336. };
  337.  
  338. var getViewer = function(prevChapter, nextChapter) {
  339. var viewerCss = toStyleStr({
  340. 'background-color': 'black'
  341. , 'text-align': 'center'
  342. , 'font': '.9em sans-serif'
  343. })
  344. , imagesCss = toStyleStr({
  345. 'margin': '5px 0'
  346. })
  347. , navCss = toStyleStr({
  348. 'text-decoration': 'none'
  349. , 'color': 'black'
  350. , 'background': 'linear-gradient(white, #ccc)'
  351. , 'padding': '3px 10px'
  352. , 'border': '1px solid #ccc'
  353. , 'border-radius': '5px'
  354. })
  355. ;
  356. // clear all styles and scripts
  357. var title = document.title;
  358. document.head.innerHTML = '';
  359. document.title = title;
  360. // and navigation
  361. var nav = (prevChapter ? '<a href="' + prevChapter + '" style="' + navCss + '" class="ml-chap-nav">Prev Chapter</a> ' : '') +
  362. (storeGet('mAutoload') ? '' : '<a href="" style="' + navCss + '">Exit</a> ') +
  363. (nextChapter ? '<a href="' + nextChapter + '" style="' + navCss + '" class="ml-chap-nav">Next Chapter</a>' : '');
  364. document.body.innerHTML = nav + '<div id="images" style="' + imagesCss + '"></div>' + nav;
  365. // set the viewer css
  366. document.body.setAttribute('style', viewerCss);
  367. // set up listeners for chapter navigation
  368. document.addEventListener('click', function(evt) {
  369. if(evt.target.className.indexOf('ml-chap-nav') !== -1) {
  370. log('next chapter will autoload');
  371. storeSet('autoload', 'yes');
  372. }
  373. }, false);
  374. return getEl('#images');
  375. };
  376.  
  377. var imageCss = toStyleStr({
  378. 'max-width': '100%'
  379. , 'display': 'block'
  380. , 'margin': '3px auto'
  381. });
  382.  
  383. var addImage = function(src, loc, callback) {
  384. var image = new Image();
  385. image.onerror = function() {
  386. log('failed to load ' + src);
  387. image.remove();
  388. };
  389. image.onload = callback;
  390. image.src = src;
  391. image.setAttribute('style', imageCss);
  392. loc.appendChild(image);
  393. };
  394.  
  395. var loadManga = function(imp) {
  396. var ex = extractInfo.bind(imp)
  397. , imgUrl = ex('img')
  398. , nextUrl = ex('next')
  399. , numPages = ex('numpages')
  400. , curPage = ex('curpage', {type:'index'}) || 1
  401. , nextChapter = ex('nextchap', {type:'value', val: (imp.invchap && -1) || 1})
  402. , prevChapter = ex('prevchap', {type:'value', val: (imp.invchap && 1) || -1})
  403. , xhr = new XMLHttpRequest()
  404. , d = document.implementation.createHTMLDocument()
  405. , addAndLoad = function(img, next) {
  406. addImage(img, loc);
  407. loadNextPage(next);
  408. }
  409. , getPageInfo = function() {
  410. var page = d.body;
  411. d.body.innerHTML = xhr.response;
  412. try {
  413. // find image and link to next page
  414. addAndLoad(ex('img', imp.imgmod, page), ex('next', null, page));
  415. } catch(e) {
  416. log('error getting details from next page, assuming end of chapter.');
  417. }
  418. }
  419. , loadNextPage = function(url) {
  420. if(mLoadLess && count % 10 === 0) {
  421. if(resumeUrl) {
  422. resumeUrl = null;
  423. } else {
  424. resumeUrl = url;
  425. log('waiting for user to scroll further before loading more images, loaded ' + count + ' pages so far, next url is ' + resumeUrl);
  426. return;
  427. }
  428. }
  429. if(++curPage > numPages) {
  430. log('reached "numPages" ' + numPages + ', assuming end of chapter');
  431. return;
  432. }
  433. if(lastUrl === url) {
  434. log('last url is the same as current, assuming end of chapter');
  435. return;
  436. }
  437. count += 1;
  438. lastUrl = url;
  439. if(imp.pages) {
  440. imp.pages(url, curPage, addAndLoad, ex);
  441. } else {
  442. xhr.open('get', url);
  443. xhr.onload = getPageInfo;
  444. xhr.onerror = function() {
  445. log('failed to load page, aborting', 'error');
  446. };
  447. xhr.send();
  448. }
  449. }
  450. , count = 1
  451. , lastUrl, loc, resumeUrl
  452. ;
  453.  
  454. if(!imgUrl || !nextUrl) {
  455. log('failed to retrieve ' + (!imgUrl ? 'image url' : 'next page url'), 'exit');
  456. }
  457.  
  458. loc = getViewer(prevChapter, nextChapter);
  459. if(mLoadLess) {
  460. window.onscroll = throttle(function(e) {
  461. if(!resumeUrl) return; // exit early if we don't have a position to resume at
  462. var scrollBottom = document.body.scrollHeight - ((document.body.scrollTop || document.documentElement.scrollTop) + window.innerHeight);
  463. if(scrollBottom < 4500) {
  464. log('user scroll nearing end, loading more images starting from ' + resumeUrl);
  465. loadNextPage(resumeUrl);
  466. }
  467. }, 100);
  468. }
  469. addAndLoad(imgUrl, nextUrl);
  470.  
  471. };
  472.  
  473. var pageUrl = window.location.href
  474. , btnLoadCss = toStyleStr({
  475. 'position': 'fixed'
  476. , 'bottom': 0
  477. , 'right': 0
  478. , 'padding': '5px'
  479. , 'margin': '0 10px 10px 0'
  480. , 'z-index': '99999'
  481. })
  482. , btnLoad
  483. ;
  484.  
  485. // used when switching chapters
  486. var autoload = storeGet('autoload');
  487. // manually set by user in menu
  488. var mAutoload = storeGet('mAutoload') || false;
  489. // should we load less pages at a time?
  490. var mLoadLess = storeGet('mLoadLess') === false ? false : true;
  491. // clear autoload
  492. storeDel('autoload');
  493.  
  494. // register menu commands
  495. if(typeof GM_registerMenuCommand === 'function') {
  496. GM_registerMenuCommand('ML: ' + (mAutoload ? 'Disable' : 'Enable') + ' manga autoload', function() {
  497. storeSet('mAutoload', !mAutoload);
  498. window.location.reload();
  499. });
  500. GM_registerMenuCommand('ML: Load ' + (mLoadLess ? 'full chapter in one go' : '10 pages at a time'), function() {
  501. storeSet('mLoadLess', !mLoadLess);
  502. window.location.reload();
  503. });
  504. }
  505.  
  506. log('starting...');
  507.  
  508. var success = implementations.some(function(imp) {
  509. if(imp.match && (new RegExp(imp.match, 'i')).test(pageUrl)) {
  510. if(BM_MODE || mAutoload || autoload) {
  511. setTimeout(loadManga.bind(null, imp),imp.wait || 0);
  512. return true;
  513. }
  514. // append button to dom that will trigger the page load
  515. btnLoad = createButton('Load Manga', function(evt) {
  516. loadManga(imp);
  517. this.remove();
  518. }, btnLoadCss);
  519. document.body.appendChild(btnLoad);
  520. return true;
  521. }
  522. });
  523.  
  524. if(!success) {
  525. log('no implementation for ' + pageUrl, 'error');
  526. }