Manga Loader NSFW

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, loads quickly and works on mobile devices through bookmarklet

// ==UserScript==
// @name       Manga Loader NSFW
// @namespace  http://www.fuzetsu.com/MangaLoaderNSFW
// @version    1.0.57
// @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, loads quickly and works on mobile devices through bookmarklet
// @copyright  2016+, fuzetsu
// @noframes
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// -- NSFW START
// @match *://dynasty-scans.com/chapters/*
// @match *://hentaifr.net/*
// @match *://prismblush.com/comic/*
// @match *://www.hentairules.net/galleries*/picture.php*
// @match *://pururin.us/read/*
// @match *://hitomi.la/reader/*
// @match *://*.doujins.com/*
// @match *://www.8muses.com/comix/picture/*/*/*/*
// @match *://nowshelf.com/watch/*
// @match *://nhentai.net/g/*/*
// @match *://e-hentai.org/s/*/*
// @match *://exhentai.org/s/*/*
// @match *://www.fakku.net/*/*/read*
// @match *://hentaihere.com/m/*/*/*
// @match *://www.hentaihere.com/m/*/*/*
// @match *://*.tsumino.com/Read/View/*
// @match *://www.hentaibox.net/*/*
// @match *://*.hentai-free.org/*
// @match *://*.mangafap.com/image/*
// @match *://*.hentai4manga.com/hentai_manga/*
// @match *://*.heymanga.me/manga/*
// @match *://*.simply-hentai.com/*/page/*
// @match *://*.gameofscanlation.moe/projects/*/*
// @match *://*.luscious.net/c/*/pictures/album/*/id/*
// @match *://*.hentaifox.com/g/*
// @match *://*.hentai2read.com/*/*/*
// @match *://*.hentai.ms/manga/*/*
// -- NSFW END
// -- FOOLSLIDE NSFW START
// @match *://reader.yuriproject.net/read/*
// @match *://ecchi.japanzai.com/read/*
// @match *://h.japanzai.com/read/*
// @match *://reader.japanzai.com/read/*
// @match *://yomanga.co/reader/read/*
// @match *://raws.yomanga.co/read/*
// @match *://hentai.cafe/manga/read/*
// @match *://*.yuri-ism.net/slide/read/*
// -- FOOLSLIDE NSFW END
// @require https://greasyfork.org/scripts/692-manga-loader/code/Manga%20Loader.user.js?29
// ==/UserScript==

/**
Sample Implementation:
{
    name: 'something' // name of the implementation
  , match: "^https?://domain.com/.*" // the url to react to for manga loading
  , img: '#image' // css selector to get the page's manga image
  , next: '#next_page' // css selector to get the link to the next page
  , numpages: '#page_select' // css selector to get the number of pages. elements like (select, span, etc)
  , curpage: '#page_select' // css selector to get the current page. usually the same as numPages if it's a select element
  , numchaps: '#chapters' // css selector to get the number of chapters in manga
  , curchap: '#chapters' // css selector to get the number of the current chapter
  , nextchap: '#next_chap' // css selector to get the link to the next chapter
  , prevchap: '#prev_chap' // same as above except for previous
  , wait: 3000 // how many ms to wait before auto loading (to wait for elements to load), or a css selector to keep trying until it returns an elem
  , pages: function(next_url, current_page_number, callback, extract_function) {
    // gets called requesting a certain page number (current_page_number)
    // to continue loading execute callback with img to append as first parameter and next url as second parameter
    // only really needs to be used on sites that have really unusual ways of loading images or depend on javascript
  }

  Any of the CSS selectors can be functions instead that return the desired value.
}
*/

// reusable functions to insert in implementations
var reuse = {
  encodeChinese: function(xhr) {
    xhr.overrideMimeType('text/html;charset=gbk');
  },
  na: function() {
    return 'N/A';
  }
};

var exUtil = {
  clearAllTimeouts: function() {
    var id = window.setTimeout(function() {}, 0);
    while (id--) {
      window.clearTimeout(id);
    }
  }
};

var nsfwimp = [{
  name: 'geh-and-exh',
  match: "^https?://(e-hentai|exhentai).org/s/.*/.*",
  img: '.sni > a > img, #img',
  next: '.sni > a, #i3 a',
  numpages: 'div.sn > div > span:nth-child(2)',
  curpage: 'div.sn > div > span:nth-child(1)'
}, {
  name: 'fakku',
  match: "^http(s)?://www.fakku.net/.*/.*/read",
  img: '.current-page',
  next: '.current-page',
  numpages: '.drop',
  curpage: '.drop',
  pages: function(url, num, cb, ex) {
    var firstNum = url.lastIndexOf('/'),
        lastDot = url.lastIndexOf('.');
    var c = url.charAt(firstNum);
    while (c && !/[0-9]/.test(c)) {
      c = url.charAt(++firstNum);
    }
    var curPage = parseInt(url.slice(firstNum, lastDot), 10);
    url = url.slice(0, firstNum) + ('00' + (curPage + 1)).slice(-3) + url.slice(lastDot);
    cb(url, url);
  }
}, {
  name: 'nowshelf',
  match: "^https?://nowshelf.com/watch/[0-9]*",
  img: '#image',
  next: '#image',
  numpages: function() {
    return parseInt(getEl('#page').textContent.slice(3), 10);
  },
  curpage: function() {
    return parseInt(getEl('#page > input').value, 10);
  },
  pages: function(url, num, cb, ex) {
    cb(page[num], num);
  }
}, {
  name: 'nhentai',
  match: "^https?://nhentai\\.net\\/g\\/[0-9]+/[0-9]+",
  img: '#image-container > a img',
  next: '#image-container > a',
  numpages: '.num-pages',
  curpage: '.current',
  imgmod: {
    altProp: 'data-cfsrc'
  },
}, {
  name: '8muses',
  match: "^http(s)?://www.8muses.com/comix/picture/[^/]+/[^/]+/[^/]+/.+",
  img: function(ctx) {
    var img = getEl('.photo img.image', ctx);
    return img ? img.src : getEl('#imageDir', ctx).value + getEl('#imageName', ctx).value;
  },
  next: '.photo > a',
  curpage: '#page-select-s',
  numpages: '#page-select-s'
}, {
  name: 'hitomi',
  match: "^http(s)?://hitomi.la/reader/[0-9]+.html",
  img: '#comicImages > img',
  next: '#comicImages > img',
  numpages: function() {
    return W.images.length;
  },
  curpage: function() {
    return parseInt(W.curPanel);
  },
  pages: function(url, num, cb, ex) {
    cb(W.images[num - 1].path, num);
  },
  wait: '#comicImages > img'
}, {
  name: 'doujin-moe',
  _pages: null,
  match: "^https?://doujins\.com/.+",
  img: 'img.picture',
  next: reuse.na,
  numpages: function() {
    if (!this._pages) {
      this._pages = getEls('#gallery djm').map(function(file) {
        return file.getAttribute('file').replace('static2.', 'static.');
      });
    }
    return this._pages.length;
  },
  curpage: function() {
    return parseInt(getEl('.counter').textContent.match(/^[0-9]+/)[0]);
  },
  pages: function(url, num, cb, ex) {
    cb(this._pages[num - 1], num);
  }
}, {
  name: 'pururin',
  match: "https?://pururin\\.us/read/.+",
  img: 'img.image-next',
  next: 'a.image-next',
  numpages: function() {
    return Object.keys(chapters).length;
  },
  curpage: 'option:checked',
  pages: function(url, num, cb, ex) {
    cb(chapters[num].image, num);
  }
}, {
  name: 'hentai-rules',
  match: "^https?://www\\.hentairules\\.net/galleries[0-9]*/picture\\.php.+",
  img: '#theMainImage',
  next: '#linkNext',
  imgmod: {
    altProp: 'data-src'
  },
  numpages: function(cur) {
    return parseInt(getEl('.imageNumber').textContent.replace(/([0-9]+)\/([0-9]+)/, cur ? '$1' : '$2'));
  },
  curpage: function() {
    return this.numpages(true);
  }
}, {
  name: 'ero-senmanga',
  match: "^https?://ero\\.senmanga\\.com/[^/]*/[^/]*/[0-9]*",
  img: '#picture',
  next: '#omv > table > tbody > tr:nth-child(2) > td > a',
  numpages: 'select[name=page]',
  curpage: 'select[name=page]',
  nextchap: function(prev) {
    var next = extractInfo('select[name=chapter]', {
      type: 'value',
      val: (prev ? -1 : 1)
    });
    if (next) return window.location.href.replace(/\/[^\/]*\/[0-9]+\/?$/, '') + '/' + next + '/1';
  },
  prevchap: function() {
    return this.nextchap(true);
  }
}, {
  name: 'hentaifr',
  match: "^https?://hentaifr\\.net/.+\\.php\\?id=[0-9]+",
  img: function(ctx, next) {
    var img = getEls('img[width]', ctx).filter(function(img) {
      return img.getAttribute('src').indexOf('contenu/doujinshis') !== -1;
    })[0];
    return next ? img : (img ? img.getAttribute('src') : null);
  },
  next: function(ctx) {
    var img = this.img(ctx, true);
    return img ? img.parentNode.getAttribute('href') : null;
  },
  wait: function() {
    return this.img() &&  this.next();
  }
}, {
  name: 'prism-blush',
  match: "^https?://prismblush.com/comic/.+",
  img: '#comic img',
  next: '#comic a'
}, {
  name: 'hentai-here',
  match: "^https?://(www\\.)?hentaihere.com/m/[^/]+/[0-9]+/[0-9]+",
  img: '#arf-reader-img',
  next: reuse.na,
  curpage: function() {
    return parseInt(W.rff_thisIndex);
  },
  numpages: function() {
    return W.rff_imageList.length;
  },
  pages: function(url, num, cb, ex) {
    cb(W.imageCDN + W.rff_imageList[num - 1], num);
  },
  nextchap: function() {
    return W.rff_nextChapter;
  },
  prevchap: function() {
    return W.rff_previousChapter;
  },
  curchap: function() {
    var curchap;
    getEls('ul.dropdown-menu.text-left li').some(function(li, index) {
      if(getEl('a.bg-info', li)) {
        curchap = index + 1;
      }
    });
    return curchap;
  },
  numchaps: 'ul.dropdown-menu.text-left',
  wait: 'ul.dropdown-menu.text-left'
}, {
  name: 'foolslide',
  match: "^https?://(" + [
    'reader.yuriproject.net/read/.+',
    'ecchi.japanzai.com/read/.+',
    'h.japanzai.com/read/.+',
    'reader.japanzai.com/read/.+',
    '(raws\\.)?yomanga.co(/reader)?/read/.+',
    'hentai.cafe/manga/read/.+',
    '(www\\.)?yuri-ism\\.net/slide/read/.'
  ].join('|') + ")",
  img: function() {
    return W.pages[W.current_page].url;
  },
  next: reuse.na,
  numpages: function() {
    return W.pages.length;
  },
  curpage: function() {
    return W.current_page + 1;
  },
  nextchap: function(prev) {
    var desired;
    var dropdown = getEls('ul.dropdown')[1] || getEls('ul.uk-nav')[1];
    if(!dropdown) return;
    getEls('a', dropdown).forEach(function(chap, idx, arr) {
      if(location.href.indexOf(chap.href) === 0) desired = arr[idx + (prev ? 1 : -1)];
    });
    return desired && desired.href;
  },
  prevchap: function() {
    return this.nextchap(true);
  },
  pages: function(url, num, cb, ex) {
    cb(W.pages[num - 1].url, num);
  },
  wait: function() {
    if(W.location.href.indexOf('yomanga') !== -1) {
      // select all possible variable names for data structure
      var matches = [].slice.call(document.body.innerHTML.match(/\w+\s*=\s*\[\{"id"/ig));
      // extract actual variable name from match
      var tokens = matches.map(function(m) {
        var tok = m.match(/^(\w+)\s*=/i);
        return tok && tok[1];
      });
      // determine the variable that holds the largest data structure
      var largest;
      var max = 0;
      tokens.forEach(function(token) {
        var cur = W[token];
        if(cur && cur.length > max && [].every.call(cur, function(pg) { return pg.url.endsWith(pg.filename); })) {
          max = cur.length;
          largest = cur;
        }
      });
      W.pages = largest || 'fail';
    }
    return W.pages;
  }
}, {
  name: 'tsumino',
  match: '^https?://(www\\.)?tsumino.com/Read/View/.+',
  img: '.reader-img',
  next: reuse.na,
  numpages: function(curpage) {
    return W.reader_max_page;
  },
  curpage: function() {
    return W.reader_current_pg;
  },
  pages: function(url, num, cb) {
    var self = this;
    if(!self._pages) {
      ajax({
        method: 'POST',
        url: '/Read/Load',
        data: 'q=' + W.location.href.match(/View\/(\d+)/)[1],
        responseType: 'json',
        beforeSend: function(xhr) {
          xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
        },
        onload: function(e) {
          var res = e.target.response;
          if(!res) return log('failed to load tsumino pages, site has probably been updated, report on forums', 'error');
          self._pages = res.reader_page_urls.map(function(page) {
            return W.reader_baseobj_url + '?name=' + encodeURIComponent(page);
          });
          cb(self._pages[num - 1], num);
        }
      });
    } else {
      cb(self._pages[num - 1], num);
    }
  },
  wait: '.reader-img'
}, {
  name: 'dynasty-scans',
  match: "^https?://dynasty-scans.com/chapters/.*",
  img: '#image > img',
  next: reuse.na,
  numpages: function() {
    return W.pages.length;
  },
  curpage: function() {
    return parseInt(getEl('#image > div.pages-list > a.page.active').getAttribute('href').slice(1));
  },
  nextchap: '#next_link',
  prevchap: '#prev_link',
  pages: function(url, num, cb, ex) {
    url = W.pages[num - 1].image;
    cb(url, url);
  }
}, {
  name: 'hentaibox',
  match: 'https?://www\\.hentaibox\\.net/hentai-manga/[^/]+/[0-9]+',
  img: 'td > center > a > img',
  next: 'td > center > a',
  numpages: function(cur) {
    var sel = getEl('select[name=np2]');
    if(sel) {
      var info = sel.options[0].textContent.match(/Page ([0-9]+) out of ([0-9]+)/i);
      if(info && info.length >= 3) return parseInt(cur ? info[1] : info[2]);
    }
  },
  curpage: function() {
    return this.numpages(true);
  }
}, {
  name: 'hentai-free', // trying to only show button on manga pages, but URL scheme makes it tough.
  match: "^http://hentai-free.org/(?!(?:tag|manga-doujinshi|hentai-video|hentai-wall|tags-map)/|.*\\?).+/$",
  img: function() {
    return W.pages[0];
  },
  next: reuse.na,
  numpages: function() {
    return W.pages.length;
  },
  curpage: function() {
    return 1;
  },
  wait: function() {
    var links = getEls('.gallery-item a, a.bwg_lightbox_0');
    if (links.length > 0) {
      W.pages = links.map(function(el) {
        return el.href.replace(/\?.*$/, '');
      });
      return true;
    }
    return false;
  },
  pages: function(url, num, cb, ex) {
    cb(W.pages[num - 1], num);
  }
}, {
  name: 'mangafap',
  match: "^https?://mangafap\\.com/image/.+",
  img: '#p',
  next: '#lbanner + div > a',
  numpages: 'select.span2',
  curpage: '.pagination li.active a'
}, {
  name: 'hentai4manga',
  match: "^https?://hentai4manga\\.com/hentai_manga/.+/\\d+/$",
  img: '#textboxContent img',
  next: '#textboxContent a',
  numpages: '#sl',
  curpage: '#sl'
}, {
  name: 'heymanga',
  match: "https?://(www\\.)?heymanga\\.me/manga/[^/]+/[0-9.]+/[0-9]+",
  img: '#img-content',
  next: function(context) {
    var num = this.curpage(context) + 1;
    return document.location.href.replace(/([0-9]+)$/, num);
  },
  numpages: function() {
    exUtil.clearAllTimeouts(); // site things mloader is an adblocker because it removes elems and blocks page, this removes the interval that checks for it
    return getEl('#page_list').length - 1;
  },
  curpage: function(context) {
    var match = getEls('#page_list > option[selected]', context);
    return parseInt(match[match.length - 1].value); // dumb site always marks page 1 as "selected" in addition to actual selected page
  },
  nextchap: 'section > .row.text-center > p + a, section > .row.text-center .ti-line-dotted + a',
  prevchap: 'section > .row.text-center .ti-hand-point-left + a',
  wait: '#page_list > option[selected]'
}, {
  name: 'simply-hentai',
  match: "https?://(.+\\.)?simply-hentai\\.com/.+/page/[0-9]+",
  img: function(ctx) {
    return getEl('#image span:nth-of-type(2)', ctx).dataset.src;
  },
  next: '#nextLink',
  numpages: function() {
    return parseInt(getEl('.inner-content .row .m10b.bold').textContent.match(/Page\s*\d+\s*of\s*(\d+)/)[1]);
  },
  curpage: function() {
    return parseInt(getEl('.inner-content .row .m10b.bold').textContent.match(/Page\s*(\d+)/)[1]);
  },
  wait: '#image img'
}, {
  name: 'gameofscanlation',
  match: "https?://gameofscanlation\.moe/projects/.+/.+",
  img: '.chapterPages img:first-of-type',
  next: function() {
    return location.href;
  },
  numpages: function() {
    return getEls('.chapterPages img').length;
  },
  nextchap: '.comicNextPageUrl',
  prevchap: '.comicPreviousPageUrl',
  curchap: 'select[name=chapter_list]',
  numchaps: 'select[name=chapter_list]',
  pages: function(url, num, cb, ex) {
    cb(W.pages[num - 1], location.href + '#' + num);
  },
  wait: function() {
    W.pages = getEls('.chapterPages img').map(function (el) {
      return el.src;
    });
    return W.pages && W.pages.length > 0;
  }
}, {
  name: 'luscious',
  match: "https?://luscious\\.net/c/.+?/pictures/album/.+?/id/.+",
  img: '.icon-download',
  next: '#next',
  curpage: function() {
    return parseInt(getEl('#pj_page_no').value);
  },
  numpages: '#pj_no_pictures'
}, {
  name: 'hentaifox',
  match: "https?://hentaifox\\.com/g/.+",
  img: '.gallery_content img.lazy',
  next: '.gallery_content a.next_nav',
  curpage: function() {
    return parseInt(extractInfo('.pag_info', {type: 'text'}));
  },
  numpages: function() {
    return extractInfo('.pag_info') - 2;
  }
}, {
  name: 'hentai2read',
  match: "https?://hentai2read\\.com/.+",
  img: '#arf-reader',
  next: '#arf-reader',
  curpage: function() {
    return parseInt(ARFfwk.doReader.data.index);
  },
  numpages: function() {
    return ARFfwk.doReader.data['images'].length;
  },
  nextchap: function(){
    return ARFfwk.doReader.data.nextURL;
  },
  prevchap: function(){
    return ARFfwk.doReader.data.previousURL;
  },
  pages: function(url, num, cb, ex) {
    cb(ARFfwk.doReader.getImageUrl(ARFfwk.doReader.data['images'][num - 1]), num);
  }
}, {
  name: 'hentai.ms',
  match: "http://www.hentai.ms/manga/[^/]+/.+",
  img: 'center > a > img',
  next: 'center > a:last-child',
  curpage: function() {
    return parseInt(getEl('center > a').parentNode.textContent.match(/([0-9]+)\//)[1]);
  },
  numpages: function() {
    return parseInt(getEl('center > a').parentNode.textContent.match(/\/([0-9]+)/)[1]);
  }
}];

log('loading nsfw implementations...');
MLoaderLoadImps(nsfwimp);