|
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
|
|
|
|
|
14
|
|
|
public function getFullTitleURL(string $title_string) : string { |
|
15
|
|
|
//FIXME: This does not point to the language specific title page. Should ask if it is possible to set LANG as arg? |
|
16
|
|
|
//NOTE: This points to a generic URL which will redirect according to the ID. |
|
17
|
|
|
// 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. |
|
18
|
|
|
$title_parts = explode(':--:', $title_string); |
|
19
|
|
|
return "http://bato.to/comic/_/comics/-r".$title_parts[0]; |
|
20
|
|
|
} |
|
21
|
|
|
|
|
22
|
|
|
public function getChapterData(string $title_string, string $chapter) : array { |
|
23
|
|
|
//$title_string isn't used here. |
|
24
|
|
|
|
|
25
|
|
|
$chapter_parts = explode(':--:', $chapter); |
|
26
|
|
|
return [ |
|
27
|
|
|
'url' => "http://bato.to/reader#" . $chapter_parts[0], |
|
28
|
|
|
'number' => $chapter_parts[1] |
|
29
|
|
|
]; |
|
30
|
|
|
} |
|
31
|
|
|
|
|
32
|
|
|
public function getTitleData(string $title_url, bool $firstGet = FALSE) { |
|
33
|
|
|
$titleData = []; |
|
34
|
|
|
|
|
35
|
|
|
$title_parts = explode(':--:', $title_url); |
|
36
|
|
|
$fullURL = $this->getFullTitleURL($title_url); |
|
37
|
|
|
$lang = $title_parts[1]; //TODO: Validate title_lang from array? |
|
38
|
|
|
|
|
39
|
|
|
|
|
40
|
|
|
//Bato.to is annoying and locks stuff behind auth. See: https://github.com/DakuTree/manga-tracker/issues/14#issuecomment-233830855 |
|
41
|
|
|
$cookies = [ |
|
42
|
|
|
"lang_option={$lang}", |
|
43
|
|
|
"member_id={$this->config->item('batoto_cookie_member_id')}", |
|
44
|
|
|
"pass_hash={$this->config->item('batoto_cookie_pass_hash')}" |
|
45
|
|
|
]; |
|
46
|
|
|
$content = $this->get_content($fullURL, implode("; ", $cookies), "", TRUE); |
|
47
|
|
|
|
|
48
|
|
|
$data = $this->parseTitleDataDOM( |
|
49
|
|
|
$content, |
|
|
|
|
|
|
50
|
|
|
$title_url, |
|
51
|
|
|
"//h1[@class='ipsType_pagetitle']", |
|
52
|
|
|
"//table[contains(@class, 'chapters_list')]/tbody/tr[2]", |
|
53
|
|
|
"td[last()]", |
|
54
|
|
|
"td/a[contains(@href,'reader')]", |
|
55
|
|
|
">Register now<" |
|
56
|
|
|
); |
|
57
|
|
|
if($data) { |
|
58
|
|
|
$titleData['title'] = html_entity_decode(trim($data['nodes_title']->textContent)); |
|
59
|
|
|
|
|
60
|
|
|
preg_match('/^(?:Vol\.(?<volume>\S+) )?(?:Ch.(?<chapter>[^\s:]+)(?:\s?-\s?(?<extra>[0-9]+))?):?.*/', trim($data['nodes_chapter']->nodeValue), $text); |
|
61
|
|
|
$titleData['latest_chapter'] = substr($data['nodes_chapter']->getAttribute('href'), 22) . ':--:' . ((!empty($text['volume']) ? 'v'.$text['volume'].'/' : '') . 'c'.$text['chapter'] . (!empty($text['extra']) ? '-'.$text['extra'] : '')); |
|
62
|
|
|
|
|
63
|
|
|
$dateString = $data['nodes_latest']->nodeValue; |
|
64
|
|
|
if($dateString == 'An hour ago') { |
|
65
|
|
|
$dateString = '1 hour ago'; |
|
66
|
|
|
} |
|
67
|
|
|
$titleData['last_updated'] = date("Y-m-d H:i:s", strtotime(preg_replace('/ (-|\[A\]).*$/', '', $dateString))); |
|
68
|
|
|
|
|
69
|
|
|
if($firstGet && $lang == 'English') { |
|
70
|
|
|
//FIXME: English is forced due for now. See #78. |
|
71
|
|
|
$titleData = array_merge($titleData, $this->doCustomFollow($content['body'], ['id' => $title_parts[0], 'lang' => $lang])); |
|
72
|
|
|
} |
|
73
|
|
|
} |
|
74
|
|
|
|
|
75
|
|
|
return (!empty($titleData) ? $titleData : NULL); |
|
76
|
|
|
} |
|
77
|
|
|
|
|
78
|
|
|
public function cleanTitleDataDOM(string $data) : string { |
|
79
|
|
|
$data = preg_replace('/^[\s\S]+<!-- ::: CONTENT ::: -->/', '<!-- ::: CONTENT ::: -->', $data); |
|
80
|
|
|
$data = preg_replace('/<!-- end mainContent -->[\s\S]+$/', '<!-- end mainContent -->', $data); |
|
81
|
|
|
$data = preg_replace('/<div id=\'commentsStart\' class=\'ipsBox\'>[\s\S]+$/', '</div></div><!-- end mainContent -->', $data); |
|
82
|
|
|
|
|
83
|
|
|
return $data; |
|
84
|
|
|
} |
|
85
|
|
|
|
|
86
|
|
|
//FIXME: This entire thing feels like an awful implementation....BUT IT WORKS FOR NOW. |
|
87
|
|
|
public function handleCustomFollow(callable $callback, string $data = "", array $extra = []) { |
|
88
|
|
|
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); |
|
89
|
|
|
|
|
90
|
|
|
$params = [ |
|
91
|
|
|
's' => $text['session_id'], |
|
92
|
|
|
'app' => 'core', |
|
93
|
|
|
'module' => 'ajax', |
|
94
|
|
|
'section' => 'like', |
|
95
|
|
|
'do' => 'save', |
|
96
|
|
|
'secure_key' => $text['secure_hash'], |
|
97
|
|
|
'f_app' => 'ccs', |
|
98
|
|
|
'f_area' => 'ccs_custom_database_3_records', |
|
99
|
|
|
'f_relid' => $extra['id'] |
|
100
|
|
|
]; |
|
101
|
|
|
$formData = [ |
|
102
|
|
|
'like_notify' => '0', |
|
103
|
|
|
'like_freq' => 'immediate', |
|
104
|
|
|
'like_anon' => '0' |
|
105
|
|
|
]; |
|
106
|
|
|
|
|
107
|
|
|
$cookies = [ |
|
108
|
|
|
"lang_option={$extra['lang']}", |
|
109
|
|
|
"member_id={$this->config->item('batoto_cookie_member_id')}", |
|
110
|
|
|
"pass_hash={$this->config->item('batoto_cookie_pass_hash')}" |
|
111
|
|
|
]; |
|
112
|
|
|
$content = $this->get_content('http://bato.to/forums/index.php?'.http_build_query($params), implode("; ", $cookies), "", TRUE, TRUE, $formData); |
|
113
|
|
|
|
|
114
|
|
|
$callback($content, $extra['id'], function($body) { |
|
115
|
|
|
return strpos($body, '>Unfollow<') !== FALSE; |
|
116
|
|
|
}); |
|
117
|
|
|
} |
|
118
|
|
|
public function doCustomUpdate() { |
|
119
|
|
|
$titleDataList = []; |
|
120
|
|
|
|
|
121
|
|
|
$cookies = [ |
|
122
|
|
|
"lang_option=English", //FIXME: English is forced due for now. See #78. |
|
123
|
|
|
"member_id={$this->config->item('batoto_cookie_member_id')}", |
|
124
|
|
|
"pass_hash={$this->config->item('batoto_cookie_pass_hash')}" |
|
125
|
|
|
]; |
|
126
|
|
|
$content = $this->get_content("http://bato.to/myfollows", implode("; ", $cookies), "", TRUE); |
|
127
|
|
|
if(!is_array($content)) { |
|
128
|
|
|
log_message('error', "{$this->site} /myfollows | Failed to grab URL (See above curl error)"); |
|
129
|
|
|
} else { |
|
130
|
|
|
$headers = $content['headers']; |
|
|
|
|
|
|
131
|
|
|
$status_code = $content['status_code']; |
|
132
|
|
|
$data = $content['body']; |
|
133
|
|
|
|
|
134
|
|
|
if(!($status_code >= 200 && $status_code < 300)) { |
|
135
|
|
|
log_message('error', "{$this->site} /myfollows | Bad Status Code ({$status_code})"); |
|
136
|
|
|
} else if(empty($data)) { |
|
137
|
|
|
log_message('error', "{$this->site} /myfollows | Data is empty? (Status code: {$status_code})"); |
|
138
|
|
|
} else { |
|
139
|
|
|
$data = preg_replace('/^[\s\S]+<!-- ::: CONTENT ::: -->/', '<!-- ::: CONTENT ::: -->', $data); |
|
140
|
|
|
$data = preg_replace('/<!-- end mainContent -->[\s\S]+$/', '<!-- end mainContent -->', $data); |
|
141
|
|
|
|
|
142
|
|
|
$dom = new DOMDocument(); |
|
143
|
|
|
libxml_use_internal_errors(TRUE); |
|
144
|
|
|
$dom->loadHTML($data); |
|
145
|
|
|
libxml_use_internal_errors(FALSE); |
|
146
|
|
|
|
|
147
|
|
|
$xpath = new DOMXPath($dom); |
|
148
|
|
|
$nodes_rows = $xpath->query("//table[contains(@class, 'chapters_list')]/tbody/tr[position()>1]"); |
|
149
|
|
|
if($nodes_rows->length > 0) { |
|
150
|
|
|
foreach($nodes_rows as $row) { |
|
151
|
|
|
$titleData = []; |
|
152
|
|
|
|
|
153
|
|
|
$nodes_title = $xpath->query("td[2]/a[1]", $row); |
|
154
|
|
|
$nodes_chapter = $xpath->query("td[2]/a[2]", $row); |
|
155
|
|
|
$nodes_lang = $xpath->query("td[3]/div", $row); |
|
156
|
|
|
$nodes_latest = $xpath->query("td[5]", $row); |
|
157
|
|
|
|
|
158
|
|
|
if($nodes_lang->length === 1 && $nodes_lang->item(0)->getAttribute('title') == 'English') { |
|
159
|
|
|
if($nodes_title->length === 1 && $nodes_chapter->length === 1 && $nodes_latest->length === 1) { |
|
160
|
|
|
$title = $nodes_title->item(0); |
|
161
|
|
|
|
|
162
|
|
|
preg_match('/(?<id>[0-9]+)$/', $title->getAttribute('href'), $title_url_arr); |
|
163
|
|
|
$title_url = "{$title_url_arr['id']}:--:English"; //FIXME: English is currently forced, see #78 |
|
164
|
|
|
|
|
165
|
|
|
if(!array_key_exists($title_url, $titleDataList)) { |
|
166
|
|
|
$titleData['title'] = trim($title->textContent); |
|
167
|
|
|
|
|
168
|
|
|
$chapter = $nodes_chapter->item(0); |
|
169
|
|
|
preg_match('/^(?:Vol\.(?<volume>\S+) )?(?:Ch.(?<chapter>[^\s:]+)(?:\s?-\s?(?<extra>[0-9]+))?):?.*/', trim($chapter->nodeValue), $text); |
|
170
|
|
|
$titleData['latest_chapter'] = substr($chapter->getAttribute('href'), 8) . ':--:' . ((!empty($text['volume']) ? 'v' . $text['volume'] . '/' : '') . 'c' . $text['chapter'] . (!empty($text['extra']) ? '-' . $text['extra'] : '')); |
|
171
|
|
|
|
|
172
|
|
|
$dateString = $nodes_latest->item(0)->nodeValue; |
|
173
|
|
|
if($dateString == 'An hour ago') { |
|
174
|
|
|
$dateString = '1 hour ago'; |
|
175
|
|
|
} |
|
176
|
|
|
$titleData['last_updated'] = date("Y-m-d H:i:s", strtotime(preg_replace('/ (-|\[A\]).*$/', '', $dateString))); |
|
177
|
|
|
|
|
178
|
|
|
|
|
179
|
|
|
$titleDataList[$title_url] = $titleData; |
|
180
|
|
|
} |
|
181
|
|
|
} else { |
|
182
|
|
|
log_message('error', "{$this->site}/Custom | Invalid amount of nodes (TITLE: {$nodes_title->length} | CHAPTER: {$nodes_chapter->length}) | LATEST: {$nodes_latest->length})"); |
|
183
|
|
|
} |
|
184
|
|
|
} |
|
185
|
|
|
} |
|
186
|
|
|
} else { |
|
187
|
|
|
log_message('error', "{$this->site} | Following list is empty?"); |
|
188
|
|
|
} |
|
189
|
|
|
} |
|
190
|
|
|
} |
|
191
|
|
|
return $titleDataList; |
|
192
|
|
|
} |
|
193
|
|
|
public function doCustomCheck(string $oldChapterString, string $newChapterString) { |
|
194
|
|
|
$oldChapterSegments = explode('/', $this->getChapterData('', $oldChapterString)['number']); |
|
195
|
|
|
$newChapterSegments = explode('/', $this->getChapterData('', $newChapterString)['number']); |
|
196
|
|
|
|
|
197
|
|
|
$status = $this->doCustomCheckCompare($oldChapterSegments, $newChapterSegments); |
|
198
|
|
|
|
|
199
|
|
|
return $status; |
|
200
|
|
|
} |
|
201
|
|
|
} |
|
202
|
|
|
|
This check looks for type mismatches where the missing type is
false. This is usually indicative of an error condtion.Consider the follow example
This function either returns a new
DateTimeobject or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returnedfalsebefore passing on the value to another function or method that may not be able to handle afalse.