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 * @param int $ttl * @return void */ protected function writeToCache(string $url, array $value, int $ttl = 0): void { if ($this->options['caching']) { Cache::add("URL_{$url}", array_merge($value, ['CACHE' => [ 'CACHE' => true, 'CACHED_AT' => Date::now(), 'EXPIRE_AT' => Date::now()->addSeconds(($ttl !== 0) ? $ttl : $this->options['cachingTimeout'])] ]), $this->options['cachingTimeout']); } } /** * Exec fetching * * @param string $url * @param string $method * @param string $userAgent * @param int $ttl * @return mixed|string * @throws GuzzleException */ protected function execute(string $url, string $method = 'GET', string $userAgent = "", int $ttl = 0): 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'], $ttl); 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'], $ttl); 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; return $this->execute($this->buildUrl("comics", $parameters), ttl: 15 * 60); } /** * 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), ttl: 15 * 60); } /** * 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), ttl: 24 * 60 * 60); } /** * 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), ttl: 24 * 60 * 60); } /** * 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, ttl: 24 * 60 * 60); // 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), ttl: 24 * 60 * 60); if ($this->legacyImagesFetch) { $responses['sorted'] = $this->legacyChapter($comic, $chapter); } else { $responses['sorted'] = $this->sort($responses['chapter']['contents'], $responses['chapter']['words']); } return $responses; } }