Files
cv4/app/Remote/CopyManga.php
2025-01-11 13:07:32 -05:00

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
* @deprecated
*/
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
* @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;
}
}