Completed
Push — master ( bee751...d8893c )
by Angus
02:29
created

Base_FoolSlide_Site_Model::getJSONUpdateURL()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php declare(strict_types=1); defined('BASEPATH') OR exit('No direct script access allowed');
2
3
/**
4
 * Class Tracker_Sites_Model
5
 */
6
class Tracker_Sites_Model extends CI_Model {
7 119
	public function __construct() {
8 119
		parent::__construct();
9 119
	}
10
11
	public function __get($name) {
12
		//TODO: Is this a good idea? There wasn't a good consensus on if this is good practice or not..
13
		//      It's probably a minor speed reduction, but that isn't much of an issue.
14
		//      An alternate solution would simply have a function which generates a PHP file with code to load each model. Similar to: https://github.com/shish/shimmie2/blob/834bc740a4eeef751f546979e6400fd089db64f8/core/util.inc.php#L1422
15
		if(!class_exists($name) || !(in_array(get_parent_class($name), ['Base_Site_Model', 'Base_FoolSlide_Site_Model']))) {
16
			return get_instance()->{$name};
17
		} else {
18
			$this->loadSite($name);
19
			return $this->{$name};
20
		}
21
	}
22
23
	private function loadSite(string $siteName) : void {
24
		$this->{$siteName} = new $siteName();
25
	}
26
}
27
28
abstract class Base_Site_Model extends CI_Model {
29
	public $site          = '';
30
	public $titleFormat   = '//';
31
	public $chapterFormat = '//';
32
33
	/**
34
	 * 0: No custom updater.
35
	 * 1: Uses following page.
36
	 * 2: Uses latest releases page.
37
	 */
38
	public $customType = 0;
39
40 16
	public function __construct() {
41 16
		parent::__construct();
42
43 16
		$this->load->database();
44
45 16
		$this->site = get_class($this);
46 16
	}
47
48
	/**
49
	 * Generates URL to the title page of the requested series.
50
	 *
51
	 * NOTE: In some cases, we are required to store more data in the title_string than is needed to generate the URL. (Namely as the title_string is our unique identifier for that series)
52
	 *       When storing additional data, we use ':--:' as a delimiter to separate the data. Make sure to handle this as needed.
53
	 *
54
	 * Example:
55
	 *    return "http://mangafox.me/manga/{$title_url}/";
56
	 *
57
	 * Example (with extra data):
58
	 *    $title_parts = explode(':--:', title_url);
59
	 *    return "https://bato.to/comic/_/comics/-r".$title_parts[0];
60
	 *
61
	 * @param string $title_url
62
	 * @return string
63
	 */
64
	abstract public function getFullTitleURL(string $title_url) : string;
65
66
	/**
67
	 * Generates chapter data from given $title_url and $chapter.
68
	 *
69
	 * Chapter must be in a (v[0-9]+/)?c[0-9]+(\..+)? format.
70
	 *
71
	 * NOTE: In some cases, we are required to store the chapter number, and the segment required to generate the chapter URL separately.
72
	 *       Much like when generating the title URL, we use ':--:' as a delimiter to separate the data. Make sure to handle this as needed.
73
	 *
74
	 * Example:
75
	 *     return [
76
	 *        'url'    => $this->getFullTitleURL($title_url).'/'.$chapter,
77
	 *        'number' => "c{$chapter}"
78
	 *    ];
79
	 *
80
	 * @param string $title_url
81
	 * @param string $chapter
82
	 * @return array [url, number]
83
	 */
84
	abstract public function getChapterData(string $title_url, string $chapter) : array;
85
86
	/**
87
	 * Used to get the latest chapter of given $title_url.
88
	 *
89
	 * This <should> utilize both get_content and parseTitleDataDOM functions when possible, as these can both reduce a lot of the code required to set this up.
90
	 *
91
	 * $titleData params must be set accordingly:
92
	 * * `title` should always be used with html_entity_decode.
93
	 * * `latest_chapter` must match $this->chapterFormat.
94
	 * * `last_updated` should always be in date("Y-m-d H:i:s") format.
95
	 * * `followed` should never be set within via getTitleData, with the exception of via a array_merge with doCustomFollow.
96
	 *
97
	 * $firstGet is set to true when the series is first added to the DB, and is used to follow the series on given site (if possible).
98
	 *
99
	 * @param string $title_url
100
	 * @param bool   $firstGet
101
	 * @return array|null [title,latest_chapter,last_updated,followed?]
102
	 */
103
	abstract public function getTitleData(string $title_url, bool $firstGet = FALSE) : ?array;
104
105
	/**
106
	 * Validates given $title_url against titleFormat.
107
	 *
108
	 * Failure to match against titleFormat will stop the series from being added to the DB.
109
	 *
110
	 * @param string $title_url
111
	 * @return bool
112
	 */
113 2
	final public function isValidTitleURL(string $title_url) : bool {
114 2
		$success = (bool) preg_match($this->titleFormat, $title_url);
115 2
		if(!$success) log_message('error', "Invalid Title URL ({$this->site}): {$title_url}");
116 2
		return $success;
117
	}
118
119
	/**
120
	 * Validates given $chapter against chapterFormat.
121
	 *
122
	 * Failure to match against chapterFormat will stop the chapter being updated.
123
	 *
124
	 * @param string $chapter
125
	 * @return bool
126
	 */
127 2
	final public function isValidChapter(string $chapter) : bool {
128 2
		$success = (bool) preg_match($this->chapterFormat, $chapter);
129 2
		if(!$success) log_message('error', "Invalid Chapter ({$this->site}): {$chapter}");
130 2
		return $success;
131
	}
132
133
	/**
134
	 * Used by getTitleData (& similar functions) to get the requested page data.
135
	 *
136
	 * @param string $url
137
	 * @param string $cookie_string
138
	 * @param string $cookiejar_path
139
	 * @param bool   $follow_redirect
140
	 * @param bool   $isPost
141
	 * @param array  $postFields
142
	 *
143
	 * @return array|bool
144
	 */
145
	final protected function get_content(string $url, string $cookie_string = "", string $cookiejar_path = "", bool $follow_redirect = FALSE, bool $isPost = FALSE, array $postFields = []) {
146
		$ch = curl_init();
147
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
148
		curl_setopt($ch, CURLOPT_ENCODING , "gzip");
149
		//curl_setopt($ch, CURLOPT_VERBOSE, 1);
150
		curl_setopt($ch, CURLOPT_HEADER, 1);
151
152
		if($follow_redirect)        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
153
154
		if(!empty($cookie_string))  curl_setopt($ch, CURLOPT_COOKIE, $cookie_string);
155
		if(!empty($cookiejar_path)) curl_setopt($ch, CURLOPT_COOKIEFILE, $cookiejar_path);
156
157
		//Some sites check the useragent for stuff, use a pre-defined user-agent to avoid stuff.
158
		curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2824.0 Safari/537.36');
159
160
		//NOTE: This is required for SSL URLs for now. Without it we tend to get error code 60.
161
		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); //FIXME: This isn't safe, but it allows us to grab SSL URLs
162
163
		curl_setopt($ch, CURLOPT_URL, $url);
164
165
		if($isPost) {
166
			curl_setopt($ch,CURLOPT_POST, count($postFields));
167
			curl_setopt($ch,CURLOPT_POSTFIELDS, http_build_query($postFields));
168
		}
169
170
		$response = curl_exec($ch);
171
		if($response === FALSE) {
172
			log_message('error', "curl failed with error: ".curl_errno($ch)." | ".curl_error($ch));
173
			//FIXME: We don't always account for FALSE return
174
			return FALSE;
175
		}
176
177
		$status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
178
		$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
179
		$headers     = http_parse_headers(substr($response, 0, $header_size));
180
		$body        = substr($response, $header_size);
181
		curl_close($ch);
182
183
		return [
184
			'headers'     => $headers,
185
			'status_code' => $status_code,
186
			'body'        => $body
187
		];
188
	}
189
190
	/**
191
	 * Used by getTitleData to get the title, latest_chapter & last_updated data from the data returned by get_content.
192
	 *
193
	 * parseTitleDataDOM checks if the data returned by get_content is valid via a few simple checks.
194
	 * * If the request was actually successful, had a valid status code & data wasn't empty. We also do an additional check on an optional $failure_string param, which will throw a failure if it's matched.
195
	 *
196
	 * Data is cleaned by cleanTitleDataDOM prior to being passed to DOMDocument.
197
	 *
198
	 * All $node_* params must be XPath to the requested node, and must only return 1 result. Anything else will throw a failure.
199
	 *
200
	 * @param array  $content
201
	 * @param string $title_url
202
	 * @param string $node_title_string
203
	 * @param string $node_row_string
204
	 * @param string $node_latest_string
205
	 * @param string $node_chapter_string
206
	 * @param string $failure_string
207
	 * @return DOMElement[]|false [nodes_title,nodes_chapter,nodes_latest]
208
	 */
209
	final protected function parseTitleDataDOM(
210
		$content, string $title_url,
211
		string $node_title_string, string $node_row_string,
212
		string $node_latest_string, string $node_chapter_string,
213
		string $failure_string = "") {
214
215
		if(!is_array($content)) {
216
			log_message('error', "{$this->site} : {$title_url} | Failed to grab URL (See above curl error)");
217
		} else {
218
			list('headers' => $headers, 'status_code' => $status_code, 'body' => $data) = $content;
0 ignored issues
show
Unused Code introduced by
The assignment to $headers is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
219
220
			if(!($status_code >= 200 && $status_code < 300)) {
221
				log_message('error', "{$this->site} : {$title_url} | Bad Status Code ({$status_code})");
222
			} else if(empty($data)) {
223
				log_message('error', "{$this->site} : {$title_url} | Data is empty? (Status code: {$status_code})");
224
			} else if($failure_string !== "" && strpos($data, $failure_string) !== FALSE) {
225
				log_message('error', "{$this->site} : {$title_url} | Failure string matched");
226
			} else {
227
				$data = $this->cleanTitleDataDOM($data); //This allows us to clean the DOM prior to parsing. It's faster to grab the only part we need THEN parse it.
228
229
				$dom = new DOMDocument();
230
				libxml_use_internal_errors(TRUE);
231
				$dom->loadHTML('<?xml encoding="utf-8" ?>' . $data);
232
				libxml_use_internal_errors(FALSE);
233
234
				$xpath = new DOMXPath($dom);
235
				$nodes_title = $xpath->query($node_title_string);
236
				$nodes_row   = $xpath->query($node_row_string);
237
				if($nodes_title->length === 1 && $nodes_row->length === 1) {
238
					$firstRow      = $nodes_row->item(0);
239
					$nodes_latest  = $xpath->query($node_latest_string,  $firstRow);
240
241
					if($node_chapter_string !== '') {
242
						$nodes_chapter = $xpath->query($node_chapter_string, $firstRow);
243
					} else {
244
						$nodes_chapter = $nodes_row;
245
					}
246
247
					if($nodes_latest->length === 1 && $nodes_chapter->length === 1) {
248
						return [
249
							'nodes_title'   => $nodes_title->item(0),
250
							'nodes_latest'  => $nodes_latest->item(0),
251
							'nodes_chapter' => $nodes_chapter->item(0)
252
						];
253
					} else {
254
						log_message('error', "{$this->site} : {$title_url} | Invalid amount of nodes (LATEST: {$nodes_latest->length} | CHAPTER: {$nodes_chapter->length})");
255
					}
256
				} else {
257
					log_message('error', "{$this->site} : {$title_url} | Invalid amount of nodes (TITLE: {$nodes_title->length} | ROW: {$nodes_row->length})");
258
				}
259
			}
260
		}
261
262
		return FALSE;
263
	}
264
265
	/**
266
	 * Used by parseTitleDataDOM to clean the data prior to passing it to DOMDocument & DOMXPath.
267
	 * This is mostly done as an (assumed) speed improvement due to the reduced amount of DOM to parse, or simply just making it easier to parse with XPath.
268
	 *
269
	 * @param string $data
270
	 * @return string
271
	 */
272
	public function cleanTitleDataDOM(string $data) : string {
273
		return $data;
274
	}
275
276
	/**
277
	 * Used to follow a series on given site if supported.
278
	 *
279
	 * This is called by getTitleData if $firstGet is true (which occurs when the series is first being added to the DB).
280
	 *
281
	 * Most of the actual following is done by handleCustomFollow.
282
	 *
283
	 * @param string $data
284
	 * @param array  $extra
285
	 * @return array
286
	 */
287
	final public function doCustomFollow(string $data = "", array $extra = []) : array {
288
		$titleData = [];
289
		$this->handleCustomFollow(function($content, $id, closure $successCallback = NULL) use(&$titleData) {
290
			if(is_array($content)) {
291
				if(array_key_exists('status_code', $content)) {
292
					$statusCode = $content['status_code'];
293
					if($statusCode === 200) {
294
						$isCallable = is_callable($successCallback);
295
						if(($isCallable && $successCallback($content['body'])) || !$isCallable) {
296
							$titleData['followed'] = 'Y';
297
298
							log_message('info', "doCustomFollow succeeded for {$id}");
299
						} else {
300
							log_message('error', "doCustomFollow failed (Invalid response?) for {$id}");
301
						}
302
					} else {
303
						log_message('error', "doCustomFollow failed (Invalid status code ({$statusCode})) for {$id}");
304
					}
305
				} else {
306
					log_message('error', "doCustomFollow failed (Missing status code?) for {$id}");
307
				}
308
			} else {
309
				log_message('error', "doCustomFollow failed (Failed request) for {$id}");
310
			}
311
		}, $data, $extra);
312
		return $titleData;
313
	}
314
315
	/**
316
	 * Used by doCustomFollow to handle following series on sites.
317
	 *
318
	 * Uses get_content to get data.
319
	 *
320
	 * $callback must return ($content, $id, closure $successCallback = NULL).
321
	 * * $content is simply just the get_content data.
322
	 * * $id is the dbID. This should be passed by the $extra arr.
323
	 * * $successCallback is an optional success check to make sure the series was properly followed.
324
	 *
325
	 * @param callable $callback
326
	 * @param string   $data
327
	 * @param array    $extra
328
	 */
329
	public function handleCustomFollow(callable $callback, string $data = "", array $extra = []) {}
330
331
	/**
332
	 * Used to check the sites following page for new updates (if supported).
333
	 * This should work much like getTitleData, but instead checks the following page.
334
	 *
335
	 * This must return an array containing arrays of each of the chapters data.
336
	 */
337
	public function doCustomUpdate() {}
338
339
	/**
340
	 * Used by the custom updater to check if a chapter looks newer than the current one.
341
	 *
342
	 * This calls doCustomCheckCompare which handles the majority of the checking.
343
	 * NOTE: Depending on the site, you may need to call getChapterData to get the chapter number to be used with this.
344
	 *
345
	 * @param string $oldChapter
346
	 * @param string $newChapter
347
	 */
348
	public function doCustomCheck(string $oldChapter, string $newChapter) {}
349
350
	/**
351
	 * Used by doCustomCheck to check if a chapter looks newer than the current one.
352
	 * Chapter must be in a (v[0-9]+/)?c[0-9]+(\..+)? format.
353
	 *
354
	 * To avoid issues with the occasional off case, this will only ever return true if we are 100% sure that the new chapter is newer than the old one.
355
	 *
356
	 * @param array $oldChapterSegments
357
	 * @param array $newChapterSegments
358
	 * @return bool
359
	 */
360 12
	final public function doCustomCheckCompare(array $oldChapterSegments, array $newChapterSegments) : bool {
361
		//NOTE: We only need to check against the new chapter here, as that is what is used for confirming update.
362 12
		$status = FALSE;
363
364
		//Make sure we have a volume element
365 12
		if(count($oldChapterSegments) === 1) array_unshift($oldChapterSegments, 'v0');
366 12
		if(count($newChapterSegments) === 1) array_unshift($newChapterSegments, 'v0');
367
368 12
		$oldCount = count($oldChapterSegments);
369 12
		$newCount = count($newChapterSegments);
370 12
		if($newCount === $oldCount) {
371
			//Make sure chapter format looks correct.
372
			//NOTE: We only need to check newCount as we know oldCount is the same count.
373 12
			if($newCount === 2) {
374
				//FIXME: Can we loop this?
375 12
				$oldVolume = substr(array_shift($oldChapterSegments), 1);
376 12
				$newVolume = substr(array_shift($newChapterSegments), 1);
377
378
				//Forcing volume to 0 as TBD might not be the latest (although it can be, but that is covered by other checks)
379 12
				if(in_array($oldVolume, ['TBD', 'TBA', 'NA', 'LMT'])) $oldVolume = 0;
380 12
				if(in_array($newVolume, ['TBD', 'TBA', 'NA', 'LMT'])) $newVolume = 0;
381
382 12
				$oldVolume = floatval($oldVolume);
383 12
				$newVolume = floatval($newVolume);
384
			} else {
385
				$oldVolume = 0;
386
				$newVolume = 0;
387
			}
388 12
			$oldChapter = floatval(substr(array_shift($oldChapterSegments), 1));
389 12
			$newChapter = floatval(substr(array_shift($newChapterSegments), 1));
390
391 12
			if($newChapter > $oldChapter && ($oldChapter >= 10 && $newChapter >= 10)) {
392
				//$newChapter is higher than $oldChapter AND $oldChapter and $newChapter are both more than 10
393
				//This is intended to cover the /majority/ of valid updates, as we technically shouldn't have to check volumes.
394
395 4
				$status = TRUE;
396 8
			} elseif($newVolume > $oldVolume && ($oldChapter < 10 && $newChapter < 10)) {
397
				//This is pretty much just to match a one-off case where the site doesn't properly increment chapter numbers across volumes, and instead does something like: v1/c1..v1/c5, v2/c1..v1/c5 (and so on).
398 1
				$status = TRUE;
399 7
			} elseif($newVolume > $oldVolume && $newChapter >= $oldChapter) {
400
				//$newVolume is higher, and chapter is higher so no need to check chapter.
401 2
				$status = TRUE;
402 5
			} elseif($newChapter > $oldChapter) {
403
				//$newVolume isn't higher, but chapter is.
404
				$status = TRUE;
405
			}
406
		}
407
408 12
		return $status;
409
	}
410
}
411
412
abstract class Base_FoolSlide_Site_Model extends Base_Site_Model {
413
	public $titleFormat   = '/^[a-z0-9_-]+$/';
414
	public $chapterFormat = '/^en(?:-us)?\/[0-9]+(?:\/[0-9]+(?:\/[0-9]+(?:\/[0-9]+)?)?)?$/';
415
	public $customType    = 2;
416
417
	public $baseURL = '';
418
419
	public function getFullTitleURL(string $title_url) : string {
420
		return "{$this->baseURL}/series/{$title_url}";
421
	}
422
423
	public function getChapterData(string $title_url, string $chapter) : array {
424
		$chapter_parts = explode('/', $chapter); //returns #LANG#/#VOLUME#/#CHAPTER#/#CHAPTER_EXTRA#(/#PAGE#/)
425
		return [
426
			'url'    => $this->getChapterURL($title_url, $chapter),
427
			'number' => ($chapter_parts[1] !== '0' ? "v{$chapter_parts[1]}/" : '') . "c{$chapter_parts[2]}" . (isset($chapter_parts[3]) ? ".{$chapter_parts[3]}" : '')/*)*/
428
		];
429
	}
430
	public function getChapterURL(string $title_url, string $chapter) : string {
431
		return "{$this->baseURL}/read/{$title_url}/{$chapter}/";
432
	}
433
434
	public function getTitleData(string $title_url, bool $firstGet = FALSE) : ?array {
435
		$titleData = [];
436
437
		$jsonURL = $this->getJSONTitleURL($title_url);
438
		if($content = $this->get_content($jsonURL)) {
439
			$json = json_decode($content['body'], TRUE);
440
			if($json && isset($json['chapters']) && count($json['chapters']) > 0) {
441
				$titleData['title'] = trim($json['comic']['name']);
442
443
				//FoolSlide title API doesn't appear to let you sort (yet every other API method which has chapters does, so we need to sort ourselves..
444
				usort($json['chapters'], function($a, $b) {
445
					return floatval("{$b['chapter']['chapter']}.{$b['chapter']['subchapter']}") <=> floatval("{$a['chapter']['chapter']}.{$a['chapter']['subchapter']}");
446
				});
447
				$latestChapter = reset($json['chapters'])['chapter'];
448
449
				$latestChapterString = "{$latestChapter['language']}/{$latestChapter['volume']}/{$latestChapter['chapter']}";
450
				if($latestChapter['subchapter'] !== '0') {
451
					$latestChapterString .= "/{$latestChapter['subchapter']}";
452
				}
453
				$titleData['latest_chapter'] = $latestChapterString;
454
455
				//No need to use date() here since this is already formatted as such.
456
				$titleData['last_updated'] = ($latestChapter['updated'] !== '0000-00-00 00:00:00' ? $latestChapter['updated'] : $latestChapter['created']);
457
			}
458
		}
459
460
		return (!empty($titleData) ? $titleData : NULL);
461
	}
462
463
	//Since we're just checking the latest updates page and not a following page, we just need to simulate a follow.
464
	//TODO: It would probably be better to have some kind of var which says that the custom update uses a following page..
465
	public function handleCustomFollow(callable $callback, string $data = "", array $extra = []) {
466
		$content = ['status_code' => 200];
467
		$callback($content, $extra['id']);
468
	}
469
	public function doCustomUpdate() {
470
		$titleDataList = [];
471
472
		$jsonURL = $this->getJSONUpdateURL();
473
		if(($content = $this->get_content($jsonURL)) && $content['status_code'] == 200) {
474
			$json = json_decode($content['body'], TRUE);
475
476
			//This should fix edge cases where chapters are uploaded in bulk in the wrong order (HelveticaScans does this with Mousou Telepathy).
477
			usort($json['chapters'], function($a, $b) {
478
				$a_date = new DateTime($a['chapter']['updated'] !== '0000-00-00 00:00:00' ? $a['chapter']['updated'] : $a['chapter']['created']);
479
				$b_date = new DateTime($b['chapter']['updated'] !== '0000-00-00 00:00:00' ? $b['chapter']['updated'] : $b['chapter']['created']);
480
				return $b_date <=> $a_date;
481
			});
482
483
			$parsedTitles = [];
484
			foreach($json['chapters'] as $chapterData) {
485
				if(!in_array($chapterData['comic']['stub'], $parsedTitles)) {
486
					$parsedTitles[] = $chapterData['comic']['stub'];
487
488
					$titleData = [];
489
					$titleData['title'] = trim($chapterData['comic']['name']);
490
491
					$latestChapter = $chapterData['chapter'];
492
493
					$latestChapterString = "en/{$latestChapter['volume']}/{$latestChapter['chapter']}";
494
					if($latestChapter['subchapter'] !== '0') {
495
						$latestChapterString .= "/{$latestChapter['subchapter']}";
496
					}
497
					$titleData['latest_chapter'] = $latestChapterString;
498
499
					//No need to use date() here since this is already formatted as such.
500
					$titleData['last_updated'] = ($latestChapter['updated'] !== '0000-00-00 00:00:00' ? $latestChapter['updated'] : $latestChapter['created']);
501
502
					$titleDataList[$chapterData['comic']['stub']] = $titleData;
503
				} else {
504
					//We already have title data for this title.
505
					continue;
506
				}
507
			}
508
		} else {
509
			log_message('error', "Custom updating failed for {$this->baseURL}.");
510
		}
511
512
		return $titleDataList;
513
	}
514
	public function doCustomCheck(string $oldChapterString, string $newChapterString) {
515
		$oldChapterSegments = explode('/', $this->getChapterData('', $oldChapterString)['number']);
516
		$newChapterSegments = explode('/', $this->getChapterData('', $newChapterString)['number']);
517
518
		$status = $this->doCustomCheckCompare($oldChapterSegments, $newChapterSegments);
519
520
		return $status;
521
	}
522
523
	public function getJSONTitleURL(string $title_url) : string {
524
		return "{$this->baseURL}/api/reader/comic/stub/{$title_url}/format/json";
525
	}
526
	public function getJSONUpdateURL() : string {
527
		return "{$this->baseURL}/api/reader/chapters/orderby/desc_created/format/json";
528
	}
529
}
530