#!/usr/bin/php
<?php

// manga-dl
// Usage: manga-dl "http://..."

// Tests:
// http://www.mangabird.com/genshiken-86
// http://www.mangainn.com/manga/chapter/10299_genshiken-chapter-25
// http://mangafox.me/manga/genshiken_nidaime_the_society_for_the_study_of_modern_visual_culture_ii/v15/c086/1.html
// http://www.mangareader.net/genshiken/85
// http://www.mangatraders.com/view/file/151999
// http://g.e-hentai.org/g/478977/e3e87498ff/
// http://mangadoom.com/Genshiken-Nidaime/90
// http://www.mangastream.to/genshiken-chapter-91.html
// http://www.batoto.net/read/_/246734/genshiken-nidaime-the-society-for-the-study-of-modern-visual-culture-ii_v17_ch99_by_no-group
// http://bato.to/read/_/270175/genshiken-nidaime-the-society-for-the-study-of-modern-visual-culture-ii_v17_ch103_by_no-group
// http://nhentai.net/g/144961/
// http://bato.to/reader#321046bc8a5e42c3
// http://hitomi.la/galleries/878737.html
// http://hitomi.la/galleries/878592.html

error_reporting(E_ALL);
ini_set('display_errors', 'On');

define('MANGADL_USER_AGENT', 'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko');

//

abstract class MangaSite {
	public $pages;

	protected function __construct() {}
	
	public static function tryCreate($url) {
		die("must override\n"); // php doesn't like abstract static
	}
	
	public abstract function getTitle();
	
	public abstract function getImgURL($page);
	
	public function getAll() {
		for ($i = 1; $i != ($this->pages + 1); $i++) {
			echo sprintf("[%3d] ", $i);
			
			$exists_filename = null;
			if (file_exists($this->filename($i, 'jpg'))) {
				$exists_filename = $this->filename($i, 'jpg');
			} elseif (file_exists($this->filename($i, 'png'))) {
				$exists_filename = $this->filename($i, 'png');
			}
			
			if ($exists_filename !== null & filesize($exists_filename) != 0) {
				echo "exists, skipping\n";
				
			} else {
				$referer = null;
				$imageurl = $this->getImgURL($i);
				if (is_array($imageurl)) {
					list($imageurl, $referer) = $imageurl;
				}
				
				echo $imageurl."... ";
				file_put_contents($this->filename($i, fileext($imageurl)), get($imageurl, $referer));
				echo "done\n";
			}
		}
		echo "All pages downloaded.\n";
	}
	
	public function filename($page, $extension='jpg') {
		return safename($this->getTitle()).'_'.sprintf("%04d", $page).'.'.$extension;
	}
}

// 

class MangaBird extends MangaSite {
	
	private $series;
	private $chapter;
	
	public static function tryCreate($url) {
		$matches = array();
		if (preg_match('~^http://www.mangabird.com/([^-]+)-(.+)/?$~', $url, $matches)) {
			$mb = new MangaBird;
			$mb->series  = $matches[1];
			$mb->chapter = $matches[2];
			
			$page = get($url);
			preg_match_all('/pageslink"\>(\d+)\</', $page, $matches);
			$mb->pages = intval(end($matches[1])) + 1;
			
			return $mb;
		} else return false;
	}
	
	public function getTitle() {
		return urldecode($this->series).' Chapter '.$this->chapter;
	}
	
	public function getImgURL($page) {
		// We are passed page as 1-indexed, mangabird takes 0-indexed
		$body = get('http://www.mangabird.com/'.$this->series.'-'.$this->chapter.'?page='.($page-1));
		
		$matches = array();
		preg_match('/<img[^>]*src="([^"]+)" alt/', $body, $matches);	
		return $matches[1];
	}
}

//

class MangaHitomi extends MangaSite {
	
	protected $gallery;
	protected $title;
	protected $imagenames;
	
	public static function tryCreate($url) {
		$matches = [];
		if (preg_match('~^http://hitomi.la/galleries/(\\d+)\.html$~', $url, $matches)) {
			$mh = new MangaHitomi;
			$mh->gallery = $matches[1];
			
			$main_page = get($url);
			preg_match('~<a href="/reader/\\d+\.html">([^<]+)</a>~', $main_page, $matches);
			$mh->title = unhesc($matches[1]);
			
			preg_match_all('~tn\.hitomi\.la/smalltn/\\d+/(.+)\.jpg\',~', $main_page, $matches);
			$mh->imagenames = $matches[1];
			$mh->pages = count($mh->imagenames);
			
			return $mh;
			
		} else return false;
	}
	
	public function getTitle() {
		return $this->title;
	}
	
	public function getImgURL($page) {
		return [
			'http://hitomi.la/galleries/'.$this->gallery.'/'.$this->imagenames[$page-1],
			'http://hitomi.la/galleries/'.$this->gallery.'.html'
		];
	}
	
}

//

class MangaBatoto extends MangaSite {
	
	private $id;
	private $slug;
	
	public static function tryCreate($url) {
		$matches = [];
		if (preg_match('~^http://bato.to/read/_/(\\d+)/([^/]*)~', $url, $matches)) {
			$mb = new MangaBatoto;
			$mb->id   = $matches[1];
			$mb->slug = $matches[2];
			
			$page = get($url);
			preg_match_all('~>page \\d+<~', $page, $matches);
			$mb->pages = count($matches[0]) / 2;
			
			return $mb;
			
		} else return false;
	}
	
	public function getTitle() {
		return str_replace(['-', '_'], [' ', ' '], $this->slug);
	}
	
	public function getImgURL($page) {
		$body = get('http://bato.to/read/_/'.$this->id.'/'.$this->slug.'/'.$page);
		
		$matches = [];
		preg_match('~comic_page[^>]+src="([^"]+)"~', $body, $matches);
		return $matches[1];
	}
}

//

class MangaBatoto2015 extends MangaSite {
	
	protected $reader_code;
	protected $title;
	
	public static function tryCreate($url) {
		$matches = [];
		if (preg_match('~^http://bato.to/reader#([0-9a-z]+)(?:_\\d*)?$~', $url, $matches)) {
			$mb = new MangaBatoto2015;
			$mb->reader_code = $matches[1];
			
			$front_page = get('http://bato.to/reader'); // sets our cookie
			
			$areader_page = get(
				'http://bato.to/areader?id='.$mb->reader_code.'&p=1',
				'http://bato.to/reader'
			);
			
			preg_match_all('~>page (\d+)</option>~', $areader_page, $matches, PREG_SET_ORDER);
			$mb->pages = count($matches) / 2; // the list appears twice
			
			preg_match('~comic_page[^>]+alt="([^"]+)"~', $areader_page, $matches);
			$mb->title = $matches[1];
			
			return $mb;
			
		} else return false;
	}
	
	public function getTitle() {
		return preg_replace('~Page \\d*~', '', str_replace(' | Batoto!', '', $this->title));
	}
	
	public function getImgURL($page) {
		$body = get(
			'http://bato.to/areader?id='.$this->reader_code.'&p='.$page,
			'http://bato.to/reader'
		);
		
		$matches = [];
		preg_match('~comic_page[^>]+src="([^"]+)"~', $body, $matches);
		return $matches[1];
	}
	
}

//

class MangaDoom extends MangaSite {

	private $series;
	private $chapter;

	public static function tryCreate($url) {
		$matches = array();
		if (preg_match('~^http://mangadoom.com/([^/]+)/(\\d+)/?$~', $url, $matches)) {
			$md = new MangaDoom;
			$md->series = $matches[1];
			$md->chapter = $matches[2];
			
			$page = get($url);
			preg_match_all('~<a href="http://mangadoom.com/'.$md->series.'/'.$md->chapter.'/\d+/">~', $page, $matches);
			
			$md->pages = count($matches[0])-1;
			
			return $md;
		} else return false;
	}
		
	public function getTitle() {
		return str_replace('-', ' ', urldecode($this->series)).' Chapter '.$this->chapter;
	}
	
	public function getImgURL($page) {
		$body = get('http://mangadoom.com/'.$this->series.'/'.$this->chapter.'/'.$page);
		
		$matches = array();
		preg_match('~wpm_nxt_pag[^>]+>[^<]+<img src="([^"]+)"~', $body, $matches);
		return $matches[1];
	}
}

// 

class MangaStream extends MangaSite {
	private $series;
	private $chapter;
	
	public static function tryCreate($url) {
		$matches = array();
		if (preg_match('~^http://www.mangastream.to/(.+)-chapter-(\\d+)\\.html~', $url, $matches)) {
			$ms = new MangaStream;
			$ms->series = $matches[1];
			$ms->chapter = $matches[2];
						
			$page = get($url);
			preg_match('~of <b>(\d+)</b>~', $page, $matches);
			
			$ms->pages = intval($matches[1]);
			
			return $ms;
		} else return false;
	}
	
	public function getTitle() {
		return str_replace('-', ' ', urldecode($this->series)).' Chapter '.$this->chapter;
	}
	
	public function getImgURL($page) {
		$body = get('http://www.mangastream.to/'.$this->series.'-chapter-'.$this->chapter.'-page-'.$page.'.html');
		
		$matches = array();
		preg_match('~<img src="(http://img.mangastream.to[^"]+)"~', $body, $matches);
		return $matches[1];
	}
}

//

class MangaFox extends MangaSite {

	private $series;
	private $volume;
	private $chapter;
	
	public static function tryCreate($url) {
		$matches = array();
		if (preg_match('~http://(?:www.)?mangafox.me/manga/([^/]+)/v(\\d+)/c(\\d+)/1.html~', $url, $matches)) {
			$mf = new MangaFox;
			$mf->series = $matches[1];
			$mf->volume = $matches[2];
			$mf->chapter = $matches[3];
		
			$page = get($url);
			preg_match('~of (\d+)~', $page, $matches);
			$mf->pages = intval($matches[1]);
			
			return $mf;
			
		} else if (preg_match('~http://(?:www.)?mangafox.me/manga/([^/]+)/c(\\d+)/1.html~', $url, $matches)) {
			$mf = new MangaFox;
			$mf->series = $matches[1];
			$mf->volume = 'na';
			$mf->chapter = $matches[2];
		
			$page = get($url);
			preg_match('~of (\d+)~', $page, $matches);
			$mf->pages = intval($matches[1]);
			
			return $mf;
			
		} else return false;
	}
	
	public function getTitle() {
		return ucwords(urldecode(str_replace('_', ' ', $this->series))).' v'.$this->volume.'c'.$this->chapter;
	}
	
	public function getImgURL($page) {
		// Passed page as 1-indexed (correct)
		$page = get('http://mangafox.me/manga/'.$this->series.'/v'.sprintf("%02d", $this->volume).'/c'.sprintf("%03d", $this->chapter).'/'.$page.'.html');
		
		$matches = array();
		preg_match('/<img[^>]*src="([^"]+)"/', $page, $matches);
		return $matches[1];
	}
}

// 

class MangaInn extends MangaSite {
	private $desc; // 10299_genshiken-chapter-25
	
	public static function tryCreate($url) {
		$matches = array();
		if (preg_match('~http://www.mangainn.com/manga/chapter/(.+)/?~', $url, $matches)) {
			$mi = new MangaInn;
			$mi->desc = $matches[1];
			
			$page = get($url);
			preg_match('/\<b style="color:white"\>(\d+)\<\/b\>/', $page, $matches);
			$mi->pages = $matches[1];
			
			return $mi;
		} else return false;
	}
	
	public function getTitle() {
		return ucwords(str_replace('_', ' ', urldecode($this->desc)));
	}

	public function getImgURL($page) {
		// Passed page as 1-indexed (correct)
		$page = get('http://www.mangainn.com/manga/chapter/'.$this->desc.'/page_'.$page);
		
		$matches = array();
		preg_match('/<img[^>]*src="(http:\/\/static\d?.mangainn.com\/mangas\/[^"]+)"/', $page, $matches);
		
		return $matches[1];
	}
}

// 

class MangaReader extends MangaSite {
	private $series;
	private $chapter;
	
	public static function tryCreate($url) {
		$matches = array();
		if (preg_match('~http://www.mangareader.net/([^/]+)/([^/]+)/?~', $url, $matches)) {
			$mr = new MangaReader;
			$mr->series = $matches[1];
			$mr->chapter = $matches[2];
			
			$page = get($url);
			preg_match('/of (\d+)/', $page, $matches);
			$mr->pages = $matches[1];
			
			return $mr;
		} else return false;
	}
	
	public function getTitle() {
		return ucwords(str_replace('_', ' ', urldecode($this->series))).' Chapter '.$this->chapter;
	}
	
	public function getImgURL($page) {
		// Passed page as 1-indexed (correct)
		$page = get('http://www.mangareader.net/'.$this->series.'/'.$this->chapter.'/'.$page);
		
		$matches = array();
		preg_match('/<img[^>]*src="([^"]+)"/', $page, $matches);
		return $matches[1];
	}
}

// 

class MangaTraders extends MangaSite {
	private $fileid;
	private $filename;
	
	public static function tryCreate($url) {
		$matches = array();
		if (preg_match('~http://www.mangatraders.com/view/file/(.+)/?~', $url, $matches)) {
			$mt = new MangaTraders;
			$mt->fileid = $matches[1];
			
			$page = get($url);
			
			preg_match('~<option value="\d+" selected="selected">([^<]+)</option>~', $page, $matches);
			$mt->filename = unhesc($matches[1]);
			
			$pageselect = strpos($page, 'Page Select:');
			preg_match_all('~<option value="(\d+)"~', substr($page, $pageselect), $matches);
			
			$mt->pages = count($matches[1]);

			return $mt;
		} else return false;
	}
	
	public function getTitle() {
		return $this->filename;
	}
	
	public function getImgURL($page) {
		// 1-indexed (correct)
		$page = file_get_contents('http://www.mangatraders.com/view/file/'.$this->fileid.'/page/'.$page);
		
		$matches = array();
		preg_match('/<img id="image" src="([^"]+)"/', $page, $matches);
		return $matches[1];
	}
}

//

class MangaNH extends MangaSite {
	protected $id;
	protected $title;
	protected $galleryid;
	protected $is_https;
	
	public static function tryCreate($url) {
		$matches = [];
		if (preg_match('~http(s?)://nhentai.net/g/(\d+)/?~', $url, $matches)) {
			$m = new MangaNH();
			$m->is_https = ($matches[1] == 's');
			$m->id = $matches[2];
			
			$page = get($url);
			
			preg_match_all('~class="thumb-container"~', $page, $matches, PREG_SET_ORDER);
			$m->pages = count($matches);
			
			preg_match('~<h1>(.+?)</h1>~', $page, $matches);
			$m->title = unhesc($matches[1]);
			
			return $m;
		} else return false;
	}
	
	public function getTitle() {
		return $this->title;
	}
	
	public function getImgURL($page) {
		$html = get('http://nhentai.net/g/'.$this->id.'/'.$page.'/');
		$matches = [];
		
		preg_match('~img src="//(i.nhentai.net/galleries/[^"]+)"~', $html, $matches);
		
		return 'http'.($this->is_https ? 's' : '').'://'.$matches[1];
	}
}

// 

class MangaGEH extends MangaSite {
	
	private $title;	
	private $page_urls = [];
	
	public static function tryCreate($url) {
		$matches = array();
		if (preg_match('~http://g.e-hentai.org/g/([^/]+)/([^/]+)/?~', $url, $matches)) {
			$ms = new MangaGEH;
			
			$page = get($url);
			
			preg_match('/id="gn">([^<]+)</', $page, $matches);
			$ms->title = $matches[1];
			
			preg_match('/of (\d+) images/', $page, $matches);
			$ms->pages = $matches[1];
			
			preg_match('~a href="(http://g\.e-hentai\.org/s/[^"]+)"~', $page, $matches);
			$ms->page_urls[1] = $matches[1];
			
			return $ms;
			
		} else return false;
	}
	
	public function getTitle() {
		return $this->title;
	}
	
	public function getImgUrl($page) {
		$matches = [];
		
		if (isset($this->page_urls[$page])) {			
			$body = file_get_contents($this->page_urls[$page]);
			
			preg_match('/id="next"[^>]+href="([^"]+)"/', $body, $matches);
			$this->page_urls[ $page + 1 ] = $matches[1]; // next page URL
			
			preg_match('/<img id="img" src="([^"]+)"/', $body, $matches);
			return $matches[1];			
			
		} else {
			echo "WARNING: Image URL unknown for page ".$page."\n";
			return false;
		}
	}
	
}

//

function MangaSite_createFromURL($url) {
	foreach(get_declared_classes() as $className) {
		if (substr($className, 0, 5) === 'Manga' && $className != 'MangaSite') {
			$attempt = call_user_func([$className, 'tryCreate'], $url);
			if ($attempt !== false) {
				return $attempt;
			}
		}
	}
	return false;
}

function safename($sz) {
	return preg_replace('~[^[:alnum:]]~u', '_', $sz);
}

function unhesc($sz) {
	return html_entity_decode( $sz, ENT_QUOTES|ENT_HTML5, 'UTF-8' );
}

function get($url, $referer=null) {
	static $ch = null;
	if (is_null($ch)) {
		$ch = curl_init();
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
		curl_setopt($ch, CURLOPT_HEADER, 0);
		curl_setopt($ch, CURLOPT_USERAGENT, MANGADL_USER_AGENT);
		curl_setopt($ch, CURLOPT_ENCODING, ''); // all supported
		curl_setopt($ch, CURLOPT_COOKIEFILE, '');
		curl_setopt($ch, CURLOPT_COOKIEJAR, (PHP_OS === 'Windows' ? 'NUL' : '/dev/null'));
		
		// Disable HTTPS validation. This is bad but we don't have a CA bundle to use
		curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
	}
	curl_setopt($ch, CURLOPT_URL, $url);
	
	curl_setopt($ch, CURLOPT_REFERER, is_null($referer) ? 0 : $referer); // maybe null
	
	$ret = curl_exec($ch);
	
	if ($ret === false) {
		echo "WARNING: ".curl_error($ch)."\n";
	}
	
	return $ret;
}

function fileext($sz) {
	// Intentionally not general-purpose
	$last = substr($sz, strlen($sz)-4);
	if ($last == '.png') return 'png';
	return 'jpg';
}

function tempdir($prefix) {
	$temp_dir = tempnam('', $prefix);
	unlink($temp_dir);
	mkdir($temp_dir);
	return $temp_dir;
}

function delTree($dir) { 
	$files = array_diff(scandir($dir), array('.','..')); 
	foreach ($files as $file) { 
		(is_dir("$dir/$file")) ? delTree("$dir/$file") : unlink("$dir/$file"); 
	} 
	return rmdir($dir); 
}

function zipFolder_unix($source_folder, $dest_file) {
	$output = [];
	$retval = -1;
	exec('/usr/bin/zip -r -j -X -9 '.escapeshellarg($dest_file).' '.escapeshellarg($source_folder), $output, $retval);
	return ($retval == 0);
}

function zipFolder_ext($source_folder, $dest_file) {
	$zip = new ZipArchive();
	$zip->open($dest_file, ZipArchive::CREATE);
	$zip->addGlob($source_folder.'/*', 0, ['remove_all_path' => true]);
		
	return !! $zip->close();
}

function main($argv) {
	if (! count($argv)) {
		die('Usage: manga-dl URL [--zip]');
	}
	
	$url = $argv[0];
	$ZIP_MODE  = false;
	
	if (isset($argv[1]) && $argv[1] == '--zip') {
		if (file_exists('/usr/bin/zip')) {
			$ZIP_MODE = 'zipFolder_unix';
		} elseif (extension_loaded('zip')) {
			$ZIP_MODE = 'zipFolder_ext';
		} else {
			die("FATAL: Zip capability is not available.");
		}
	}
	
	$curdir = getcwd();
	
	$manga = MangaSite_createFromURL($url);
	if ($manga === false) {
		die("No manga site handler accepted the url '$url'");
	}	
	echo $manga->getTitle().' ('.$manga->pages.' page(s))'."\n";
	
	if ($ZIP_MODE) {
		$temp_dir = tempdir('manga-dl');
		echo "Moving to temporary directory\n";
		
		chdir($temp_dir);
	}
	
	$manga->getAll();
	
	if ($ZIP_MODE) {
		chdir($curdir);
		
		echo "Adding files to zip archive...\n";
		
		$tempzip_ = tempnam('', 'manga-dl');
		$tempzip = $tempzip_.'.zip';
		
		if (call_user_func($ZIP_MODE, $temp_dir, $tempzip)) {
			
			echo "Moving zip file...\n";
			
			rename($tempzip, safename($manga->getTitle()).'.cbz');
			
			echo "Removing temporary files...\n";
			
			delTree($temp_dir);
			
		} else {
			
			echo "Failed to create zip file! Individual downloaded images were kept in path:\n";
			echo $temp_dir."\n";
			
		}
		
		unlink($tempzip_);
	}
	
	die("Complete\n");
}

return main(array_slice($_SERVER['argv'], 1));
