361 lines
10 KiB
PHP
361 lines
10 KiB
PHP
<?php
|
|
|
|
namespace App\Remote;
|
|
|
|
use DOMDocument;
|
|
use Exception;
|
|
use GuzzleHttp\Client;
|
|
use GuzzleHttp\Exception\GuzzleException;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Date;
|
|
|
|
/**
|
|
* CopyManga API
|
|
*/
|
|
class CopyManga
|
|
{
|
|
|
|
/**
|
|
* @var array Caching options
|
|
*/
|
|
protected array $options = [
|
|
'caching' => true,
|
|
'cachingTimeout' => 600
|
|
];
|
|
|
|
/**
|
|
* @var string API version
|
|
*/
|
|
protected string $apiVersion = "v3";
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected string $url = "https://api.mangacopy.com/api/";
|
|
|
|
/**
|
|
* @var string Encryption for legacy image fetch
|
|
*/
|
|
protected string $encryptionKey = "xxxmanga.woo.key";
|
|
|
|
/**
|
|
* @var bool Use old method to fetch images list
|
|
*/
|
|
public bool $legacyImagesFetch = true;
|
|
|
|
/**
|
|
* @var string User Agent for API calls
|
|
*/
|
|
protected string $userAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 15_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.3 Mobile/15E148 Safari/604.1";
|
|
|
|
/**
|
|
* Default parameters
|
|
*
|
|
* @var array
|
|
*/
|
|
protected array $defaultParameters = [
|
|
'format' => 'json',
|
|
'platform' => 1
|
|
];
|
|
|
|
/**
|
|
* @var array parameters to be used
|
|
*/
|
|
protected array $parameters = [];
|
|
|
|
/**
|
|
* Build URL for method execute to run
|
|
*
|
|
* @param string $endpoint
|
|
* @param array $parameters
|
|
* @param bool $defaultParameters
|
|
* @return string
|
|
*/
|
|
protected function buildUrl(string $endpoint, array $parameters, bool $defaultParameters = true): string
|
|
{
|
|
// Merge parameters
|
|
if ($defaultParameters) {
|
|
$this->parameters = array_merge($this->defaultParameters, $parameters);
|
|
} else {
|
|
$this->parameters = $parameters;
|
|
}
|
|
|
|
return $this->url . $this->apiVersion . "/" . $endpoint . "?" . http_build_query($this->parameters);
|
|
}
|
|
|
|
/**
|
|
* Build URL for method execute to run
|
|
*
|
|
* @param string $endpoint
|
|
* @param array $parameters
|
|
* @param bool $defaultParameters
|
|
* @return string
|
|
*/
|
|
protected function legacyBuildUrl(string $endpoint, array $parameters = [], bool $defaultParameters = true): string
|
|
{
|
|
return "https://www.mangacopy.com/" . $endpoint;
|
|
}
|
|
|
|
/**
|
|
* Write to cache
|
|
*
|
|
* @param string $url
|
|
* @param array $value
|
|
* @return void
|
|
*/
|
|
protected function writeToCache(string $url, array $value): void
|
|
{
|
|
if ($this->options['caching']) {
|
|
Cache::add("URL_{$url}", array_merge($value, ['CACHE' => [
|
|
'CACHE' => true,
|
|
'CACHED_AT' => Date::now(),
|
|
'EXPIRE_AT' => Date::now()->addSeconds($this->options['cachingTimeout'])]
|
|
]),
|
|
$this->options['cachingTimeout']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Exec fetching
|
|
*
|
|
* @param string $url
|
|
* @param string $method
|
|
* @param string $userAgent
|
|
* @return mixed|string
|
|
* @throws GuzzleException
|
|
*/
|
|
protected function execute(string $url, string $method = 'GET', string $userAgent = ""): mixed
|
|
{
|
|
if ($this->options['caching']) {
|
|
// Check cache exist
|
|
if (Cache::has("URL_{$url}")) {
|
|
$cache = Cache::get("URL_{$url}");
|
|
if (isset($cache['type']) && $cache['type'] == 'HTML') {
|
|
return $cache['response'];
|
|
} else {
|
|
return $cache;
|
|
}
|
|
}
|
|
}
|
|
|
|
$client = new Client(['headers' => ['User-Agent' => ($userAgent === "") ? $this->userAgent : $userAgent]]);
|
|
|
|
$options = [];
|
|
if ($method === 'OPTIONS') {
|
|
$options = ['headers' => ['Access-Control-Request-Method' => 'GET']];
|
|
} else {
|
|
$options = ['headers' => ['platform' => '1', 'version' => '2022.10.28', 'webp' => '0', 'region' => '0', 'Accept' => 'application/json']];
|
|
}
|
|
|
|
$response = $client->request($method, $url, $options);
|
|
|
|
if ($response->getStatusCode() !== 200) {
|
|
throw new Exception($response->getBody());
|
|
}
|
|
|
|
if ($userAgent !== "") {
|
|
// Directly send html to method to process
|
|
$html = $response->getBody()->getContents();
|
|
$this->writeToCache($url, ['response' => $html, 'type' => 'HTML']);
|
|
|
|
return $html;
|
|
}
|
|
|
|
if ($method !== "OPTIONS") {
|
|
$json = json_decode($response->getBody()->getContents(), true);
|
|
|
|
if (json_last_error() === JSON_ERROR_NONE) {
|
|
// Save to cache if needed
|
|
$this->writeToCache($url, $json['results']);
|
|
return $json['results'];
|
|
} else {
|
|
throw new Exception($json['code']);
|
|
}
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get tags available
|
|
*
|
|
* @return mixed|string
|
|
* @throws GuzzleException
|
|
*/
|
|
public function tags()
|
|
{
|
|
$parameters['type'] = 1; // From iOS site
|
|
|
|
$options = $this->execute($this->buildUrl("h5/filter/comic/tags", $parameters, false), 'OPTIONS');
|
|
return $this->execute($this->buildUrl("h5/filter/comic/tags", $parameters));
|
|
}
|
|
|
|
/**
|
|
* Get comics
|
|
*
|
|
* @param int $limit
|
|
* @param int $offset
|
|
* @param string $top
|
|
* @param array $parameters
|
|
* @return mixed|string
|
|
* @throws Exception|GuzzleException
|
|
*/
|
|
public function comics(int $limit = 28, int $offset = 0, string $top = '', array $parameters = []): mixed
|
|
{
|
|
if (isset($parameters['theme']) && $parameters['theme'] === 'all') {
|
|
$parameters['theme'] = "";
|
|
}
|
|
|
|
$parameters['limit'] = $limit;
|
|
$parameters['offset'] = $offset;
|
|
$parameters['top'] = $top;
|
|
//OPTIONS
|
|
// https://api.mangacopy.com/api/v3/comics?format=json&platform=1&q=x&limit=30&offset=0&top=all"
|
|
// https://api.mangacopy.com/api/v3/search/comic?platform=1&q=x&limit=20&offset=0&q_type=&_update=true
|
|
|
|
return $this->execute($this->buildUrl("comics", $parameters));
|
|
}
|
|
|
|
/**
|
|
* Search comic by name
|
|
*
|
|
* @param string $item
|
|
* @param int $limit
|
|
* @param int $offset
|
|
* @return mixed|string
|
|
* @throws GuzzleException
|
|
*/
|
|
public function search(string $item = '', int $limit = 28, int $offset = 0)
|
|
{
|
|
$parameters['q'] = $item;
|
|
$parameters['q_type'] = "";
|
|
$parameters['_update'] = true;
|
|
$parameters['limit'] = $limit;
|
|
$parameters['offset'] = $offset;
|
|
|
|
return $this->execute($this->buildUrl("search/comic", $parameters));
|
|
}
|
|
|
|
/**
|
|
* Get comic info
|
|
*
|
|
* @param string $comic
|
|
* @param array $parameters
|
|
* @return mixed|string
|
|
* @throws Exception|GuzzleException
|
|
*/
|
|
public function comic(string $comic, array $parameters = []): mixed
|
|
{
|
|
return $this->execute($this->buildUrl("comic2/{$comic}", $parameters));
|
|
}
|
|
|
|
/**
|
|
* Get comic chapters
|
|
*
|
|
* @param string $comic
|
|
* @param int $limit
|
|
* @param int $offset
|
|
* @param array $parameters
|
|
* @param string $group
|
|
* @return mixed|string
|
|
* @throws GuzzleException
|
|
*/
|
|
public function chapters(string $comic, int $limit = 200, int $offset = 0, array $parameters = [], string $group = "default"): mixed
|
|
{
|
|
$parameters['limit'] = $limit;
|
|
$parameters['offset'] = $offset;
|
|
$options = $this->execute($this->buildUrl("comic/{$comic}/group/{$group}/chapters", $parameters, false), 'OPTIONS');
|
|
|
|
return $this->execute($this->buildUrl("comic/{$comic}/group/{$group}/chapters", $parameters));
|
|
}
|
|
|
|
/**
|
|
* Sort images by order
|
|
*
|
|
* @param array $arrayToOrder
|
|
* @param array $keys
|
|
* @return array
|
|
*/
|
|
protected function sort(array $arrayToOrder, array $keys): array
|
|
{
|
|
$output = [];
|
|
|
|
foreach ($keys as $key => $value) {
|
|
$output[$value] = $arrayToOrder[$key];
|
|
}
|
|
|
|
ksort($output);
|
|
return $output;
|
|
}
|
|
|
|
/**
|
|
* @param string $comic
|
|
* @param string $chapter
|
|
* @return array
|
|
* @throws GuzzleException
|
|
*/
|
|
public function legacyChapter(string $comic, string $chapter): array
|
|
{
|
|
$userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36";
|
|
$responses = $this->execute($this->legacyBuildUrl("comic/{$comic}/chapter/{$chapter}"), "GET", $userAgent);
|
|
|
|
// Get Content Key
|
|
$dom = new DOMDocument();
|
|
$dom->loadHTML($responses);
|
|
$dataNode = $dom->getElementsByTagName("div");
|
|
|
|
$encryptedData = "";
|
|
|
|
foreach ($dataNode as $node) {
|
|
if ($node->getAttribute("class") === 'imageData') {
|
|
$encryptedData = $node->attributes->item(1)->value;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Decrypt content
|
|
$content = $this->decrypt($encryptedData);
|
|
return json_decode($content, true);
|
|
}
|
|
|
|
/**
|
|
* Decrypt content of AES-128-CBC
|
|
*
|
|
* @param string $payload
|
|
* @return bool|string
|
|
*/
|
|
protected function decrypt(string $payload): bool|string
|
|
{
|
|
// Decrypt content
|
|
$cipher = "AES-128-CBC";
|
|
$ivLength = openssl_cipher_iv_length($cipher);
|
|
|
|
$iv = substr($payload, 0, $ivLength); // First 16 char
|
|
$encryptedData = hex2bin(substr($payload, $ivLength)); // Rest of content, convert to binary
|
|
|
|
return openssl_decrypt($encryptedData, $cipher, $this->encryptionKey, OPENSSL_RAW_DATA, $iv);
|
|
}
|
|
|
|
/**
|
|
* Get image for specific chapter of comic
|
|
*
|
|
* @param string $comic
|
|
* @param string $chapter
|
|
* @param array $parameters
|
|
* @return array
|
|
* @throws Exception|GuzzleException
|
|
*/
|
|
public function chapter(string $comic, string $chapter, array $parameters = []): array
|
|
{
|
|
$responses = $this->execute($this->buildUrl("comic/{$comic}/chapter2/{$chapter}", $parameters));
|
|
$responses['sorted'] = $this->sort($responses['chapter']['contents'], $responses['chapter']['words']);
|
|
|
|
if ($this->legacyImagesFetch) {
|
|
$responses['sorted'] = $this->legacyChapter($comic, $chapter);
|
|
}
|
|
|
|
return $responses;
|
|
}
|
|
|
|
}
|