Completed
Push — master ( 222f63...75778e )
by Angus
02:21
created

Batoto   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 203
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
dl 0
loc 203
ccs 0
cts 115
cp 0
rs 10
c 0
b 0
f 0
wmc 29
lcom 1
cbo 2
1
<?php declare(strict_types=1); defined('BASEPATH') OR exit('No direct script access allowed');
2
3
class Batoto extends Base_Site_Model {
4
	//Batoto is a bit tricky to track. Unlike MangaFox and MangaHere, it doesn't store anything in the title_url, which means we have to get the data via other methods.
5
	//One problem we have though, is the tracker must support multiple sites, so this means we need to do some weird things to track Batoto.
6
	//title_url is stored like: "ID:--:LANGUAGE"
7
	//chapter_urls are stored like "CHAPTER_ID:--:CHAPTER_NUMBER"
8
9
	public $titleFormat   = '/^[0-9]+:--:(?:English|Spanish|French|German|Portuguese|Turkish|Indonesian|Greek|Filipino|Italian|Polish|Thai|Malay|Hungarian|Romanian|Arabic|Hebrew|Russian|Vietnamese|Dutch)$/';
10
	//FIXME: We're not validating the chapter name since we don't know what all the possible valid characters can be
11
	//       Preferably we'd just use /^[0-9a-z]+:--:(v[0-9]+\/)?c[0-9]+(\.[0-9]+)?$/
12
	public $chapterFormat = '/^[0-9a-z]+:--:.+$/';
13
	public $customType    = 1;
14
	public $hasCloudFlare = TRUE;
15
16
	public function getFullTitleURL(string $title_url) : string {
17
		//FIXME: This does not point to the language specific title page. Should ask if it is possible to set LANG as arg?
18
		//NOTE: This points to a generic URL which will redirect according to the ID.
19
		//      It's possible the title of a series can change, essentially making it possible for us to have multiple versions of the same title. This stops that.
20
		$title_parts = explode(':--:', $title_url);
21
		return "https://bato.to/comic/_/comics/-r{$title_parts[0]}";
22
	}
23
24
	public function getChapterData(string $title_url, string $chapter) : array {
25
		//$title_url isn't used here.
26
27
		$chapter_parts = explode(':--:', $chapter);
28
		return [
29
			'url'    => "https://bato.to/reader#" . $chapter_parts[0],
30
			'number' => $chapter_parts[1]
31
		];
32
	}
33
34
	public function getTitleData(string $title_url, bool $firstGet = FALSE) : ?array {
35
		$titleData = [];
36
37
		$title_parts = explode(':--:', $title_url);
38
		$fullURL     = $this->getFullTitleURL($title_url);
39
		$lang        = $title_parts[1]; //TODO: Validate title_lang from array?
40
41
42
		//Bato.to is annoying and locks stuff behind auth. See: https://github.com/DakuTree/manga-tracker/issues/14#issuecomment-233830855
43
		$cookies = [
44
			"lang_option={$lang}",
45
			"member_id={$this->config->item('batoto_cookie_member_id')}",
46
			"pass_hash={$this->config->item('batoto_cookie_pass_hash')}"
47
		];
48
		$content = $this->get_content($fullURL, implode("; ", $cookies), "", TRUE);
49
50
		$data = $this->parseTitleDataDOM(
51
			$content,
52
			$title_url,
53
			"//h1[@class='ipsType_pagetitle']",
54
			"//table[contains(@class, 'chapters_list')]/tbody/tr[2]",
55
			"td[last()]",
56
			"td/a[contains(@href,'reader#')]",
57
			">Register now<"
58
		);
59
		if($data) {
60
			$titleData['title'] = html_entity_decode(trim($data['nodes_title']->textContent));
61
62
			preg_match('/^(?:Vol\.(?<volume>\S+) )?(?:Ch.(?<chapter>[^\s:]+)(?:\s?-\s?(?<extra>[0-9]+))?):?.*/', trim($data['nodes_chapter']->nodeValue), $text);
63
			$chapter_url = $data['nodes_chapter']->getAttribute('href');
64
			$titleData['latest_chapter'] = substr($chapter_url, strpos($chapter_url, "reader#") + 7) . ':--:' . ((!empty($text['volume']) ? 'v'.$text['volume'].'/' : '') . 'c'.$text['chapter'] . (!empty($text['extra']) ? '-'.$text['extra'] : ''));
65
66
			$dateString = $data['nodes_latest']->nodeValue;
67
			if($dateString == 'An hour ago') {
68
				$dateString = '1 hour ago';
69
			}
70
			$titleData['last_updated'] = date("Y-m-d H:i:s", strtotime(preg_replace('/ (-|\[A\]).*$/', '', $dateString)));
71
72
			if($firstGet && $lang == 'English') {
73
				//FIXME: English is forced due for now. See #78.
74
				$titleData = array_merge($titleData, $this->doCustomFollow($content['body'], ['id' => $title_parts[0], 'lang' => $lang]));
75
			}
76
		}
77
78
		return (!empty($titleData) ? $titleData : NULL);
79
	}
80
81
	public function cleanTitleDataDOM(string $data) : string {
82
		$data = preg_replace('/^[\s\S]+<!-- ::: CONTENT ::: -->/', '<!-- ::: CONTENT ::: -->', $data);
83
		$data = preg_replace('/<!-- end mainContent -->[\s\S]+$/', '<!-- end mainContent -->', $data);
84
		$data = preg_replace('/<div id=\'commentsStart\' class=\'ipsBox\'>[\s\S]+$/', '</div></div><!-- end mainContent -->', $data);
85
86
		return $data;
87
	}
88
89
	//FIXME: This entire thing feels like an awful implementation....BUT IT WORKS FOR NOW.
90
	public function handleCustomFollow(callable $callback, string $data = "", array $extra = []) {
91
		preg_match('/ipb\.vars\[\'secure_hash\'\]\s+=\s+\'(?<secure_hash>[0-9a-z]+)\';[\s\S]+ipb\.vars\[\'session_id\'\]\s+=\s+\'(?<session_id>[0-9a-z]+)\';/', $data, $text);
92
93
		$params = [
94
			's'          => $text['session_id'],
95
			'app'        => 'core',
96
			'module'     => 'ajax',
97
			'section'    => 'like',
98
			'do'         => 'save',
99
			'secure_key' => $text['secure_hash'],
100
			'f_app'      => 'ccs',
101
			'f_area'     => 'ccs_custom_database_3_records',
102
			'f_relid'    => $extra['id']
103
		];
104
		$formData = [
105
			'like_notify' => '0',
106
			'like_freq'   => 'immediate',
107
			'like_anon'   => '0'
108
		];
109
110
		$cookies = [
111
			"lang_option={$extra['lang']}",
112
			"member_id={$this->config->item('batoto_cookie_member_id')}",
113
			"pass_hash={$this->config->item('batoto_cookie_pass_hash')}"
114
		];
115
		$content = $this->get_content('https://bato.to/forums/index.php?'.http_build_query($params), implode("; ", $cookies), "", TRUE, TRUE, $formData);
116
117
		$callback($content, $extra['id'], function($body) {
118
			return strpos($body, '>Unfollow<') !== FALSE;
119
		});
120
	}
121
	public function doCustomUpdate() {
122
		$titleDataList = [];
123
124
		$cookies = [
125
			"lang_option=English", //FIXME: English is forced due for now. See #78.
126
			"member_id={$this->config->item('batoto_cookie_member_id')}",
127
			"pass_hash={$this->config->item('batoto_cookie_pass_hash')}"
128
		];
129
		$content = $this->get_content("https://bato.to/myfollows", implode("; ", $cookies), "", TRUE);
130
		if(!is_array($content)) {
131
			log_message('error', "{$this->site} /myfollows | Failed to grab URL (See above curl error)");
132
		} else {
133
			$headers     = $content['headers'];
134
			$status_code = $content['status_code'];
135
			$data        = $content['body'];
136
137
			if(!($status_code >= 200 && $status_code < 300)) {
138
				log_message('error', "{$this->site} /myfollows | Bad Status Code ({$status_code})");
139
			} else if(empty($data)) {
140
				log_message('error', "{$this->site} /myfollows | Data is empty? (Status code: {$status_code})");
141
			} else {
142
				$data = preg_replace('/^[\s\S]+<!-- ::: CONTENT ::: -->/', '<!-- ::: CONTENT ::: -->', $data);
143
				$data = preg_replace('/<!-- end mainContent -->[\s\S]+$/', '<!-- end mainContent -->', $data);
144
145
				$dom = new DOMDocument();
146
				libxml_use_internal_errors(TRUE);
147
				$dom->loadHTML($data);
148
				libxml_use_internal_errors(FALSE);
149
150
				$xpath      = new DOMXPath($dom);
151
				$nodes_rows = $xpath->query("//table[contains(@class, 'chapters_list')]/tbody/tr[position()>1]");
152
				if($nodes_rows->length > 0) {
153
					foreach($nodes_rows as $row) {
154
						$titleData = [];
155
156
						$nodes_title   = $xpath->query("td[2]/a[1]", $row);
157
						$nodes_chapter = $xpath->query("td[2]/a[2]", $row);
158
						$nodes_lang    = $xpath->query("td[3]/div", $row);
159
						$nodes_latest  = $xpath->query("td[5]", $row);
160
161
						if($nodes_lang->length === 1 && $nodes_lang->item(0)->getAttribute('title') == 'English') {
162
							if($nodes_title->length === 1 && $nodes_chapter->length === 1 && $nodes_latest->length === 1) {
163
								$title = $nodes_title->item(0);
164
165
								preg_match('/(?<id>[0-9]+)$/', $title->getAttribute('href'), $title_url_arr);
166
								$title_url = "{$title_url_arr['id']}:--:English"; //FIXME: English is currently forced, see #78
167
168
								if(!array_key_exists($title_url, $titleDataList)) {
169
									$titleData['title'] = trim($title->textContent);
170
171
									$chapter = $nodes_chapter->item(0);
172
									preg_match('/^(?:Vol\.(?<volume>\S+) )?(?:Ch.(?<chapter>[^\s:]+)(?:\s?-\s?(?<extra>[0-9]+))?):?.*/', trim($chapter->nodeValue), $text);
173
									$chapter_url = $chapter->getAttribute('href');
174
									$titleData['latest_chapter'] = substr($chapter_url, strpos($chapter_url, "reader#") + 7) . ':--:' . ((!empty($text['volume']) ? 'v'.$text['volume'].'/' : '') . 'c'.$text['chapter'] . (!empty($text['extra']) ? '-'.$text['extra'] : ''));
175
176
									$dateString = $nodes_latest->item(0)->nodeValue;
177
									if($dateString == 'An hour ago') {
178
										$dateString = '1 hour ago';
179
									}
180
									$titleData['last_updated'] = date("Y-m-d H:i:s", strtotime(preg_replace('/ (-|\[A\]).*$/', '', $dateString)));
181
182
183
									$titleDataList[$title_url] = $titleData;
184
								}
185
							} else {
186
								log_message('error', "{$this->site}/Custom | Invalid amount of nodes (TITLE: {$nodes_title->length} | CHAPTER: {$nodes_chapter->length}) | LATEST: {$nodes_latest->length})");
187
							}
188
						}
189
					}
190
				} else {
191
					log_message('error', "{$this->site} | Following list is empty?");
192
				}
193
			}
194
		}
195
		return $titleDataList;
196
	}
197
	public function doCustomCheck(string $oldChapterString, string $newChapterString) {
198
		$oldChapterSegments = explode('/', $this->getChapterData('', $oldChapterString)['number']);
199
		$newChapterSegments = explode('/', $this->getChapterData('', $newChapterString)['number']);
200
201
		$status = $this->doCustomCheckCompare($oldChapterSegments, $newChapterSegments);
202
203
		return $status;
204
	}
205
}
206