Initial
This commit is contained in:
17
app/Enum/MimeType.php
Normal file
17
app/Enum/MimeType.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enum;
|
||||
|
||||
enum MimeType: string
|
||||
{
|
||||
|
||||
case png = 'image/png';
|
||||
case jpg = 'image/jpeg';
|
||||
case gif = 'image/gif';
|
||||
case bmp = 'image/bmp';
|
||||
case ico = 'image/vnd.microsoft.icon';
|
||||
case tiff = 'image/tiff';
|
||||
case svg = 'image/svg+xml';
|
||||
case webp = 'image/webp';
|
||||
|
||||
}
|
||||
@@ -30,7 +30,7 @@ class NewPasswordController extends Controller
|
||||
/**
|
||||
* Handle an incoming new password request.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
|
||||
@@ -25,7 +25,7 @@ class PasswordResetLinkController extends Controller
|
||||
/**
|
||||
* Handle an incoming password reset link request.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
|
||||
@@ -10,8 +10,10 @@ use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use function Laravel\Prompts\error;
|
||||
|
||||
class RegisteredUserController extends Controller
|
||||
{
|
||||
@@ -26,13 +28,13 @@ class RegisteredUserController extends Controller
|
||||
/**
|
||||
* Handle an incoming registration request.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|string|lowercase|email|max:255|unique:'.User::class,
|
||||
'email' => 'required|string|lowercase|email|max:255|contains:yumj.in|unique:'.User::class,
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
]);
|
||||
|
||||
|
||||
230
app/Http/Controllers/ComicController.php
Normal file
230
app/Http/Controllers/ComicController.php
Normal file
@@ -0,0 +1,230 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Author;
|
||||
use App\Models\Chapter;
|
||||
use App\Models\Comic;
|
||||
use App\Models\Image;
|
||||
use App\Remote\CopyManga;
|
||||
use App\Remote\ImageFetcher;
|
||||
use Illuminate\Contracts\Routing\ResponseFactory;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response as IlluminateHttpResponse;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class ComicController extends Controller
|
||||
{
|
||||
|
||||
public function __construct(private readonly CopyManga $copyManga)
|
||||
{
|
||||
}
|
||||
|
||||
public function favourites(Request $request)
|
||||
{
|
||||
$favourites = $request->user()->favourites()->with(['authors'])->orderBy('upstream_updated_at', 'desc')->get();
|
||||
|
||||
return Inertia::render('Comic/Favourites', [
|
||||
'favourites' => $favourites,
|
||||
]);
|
||||
}
|
||||
|
||||
public function postFavourite(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
// Get pathname to comic_id
|
||||
$comic = Comic::where('pathword', $request->pathword)->firstOrFail();
|
||||
} catch (ModelNotFoundException $e) {
|
||||
// Fetch from remote
|
||||
$remoteComic = $this->copyManga->comic($request->pathword);
|
||||
|
||||
$comic = new Comic;
|
||||
$comic->pathword = $remoteComic['comic']['path_word'];
|
||||
$comic->name = $remoteComic['comic']['name'];
|
||||
$comic->cover = $remoteComic['comic']['cover'];
|
||||
$comic->upstream_updated_at = $remoteComic['comic']['datetime_updated'];
|
||||
$comic->uuid = $remoteComic['comic']['uuid'];
|
||||
$comic->alias = explode(',', $remoteComic['comic']['alias']);
|
||||
$comic->description = $remoteComic['comic']['brief'];
|
||||
$comic->metadata = $remoteComic;
|
||||
$comic->save();
|
||||
}
|
||||
|
||||
// Set favourite
|
||||
if ($request->user()->favourites()->where('comic_id', $comic->id)->exists()) {
|
||||
$request->user()->favourites()->detach($comic->id);
|
||||
} else {
|
||||
$request->user()->favourites()->attach($comic->id);
|
||||
}
|
||||
|
||||
return response()->json($request->user()->favourites()->get(['pathword'])->pluck('pathword'));
|
||||
}
|
||||
|
||||
public function image(Request $request, string $url): ResponseFactory|Application|IlluminateHttpResponse
|
||||
{
|
||||
// TODO: Ref check and make it require auth
|
||||
$fetcher = new ImageFetcher(base64_decode($url));
|
||||
|
||||
return response($fetcher->fetch())->withHeaders([
|
||||
'Content-Type' => $fetcher->getMimeType()->value,
|
||||
'Cache-Control' => 'max-age=604800',
|
||||
]);
|
||||
}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$params = [];
|
||||
if ($request->has('tag')) {
|
||||
$params['theme'] = $request->get('tag');
|
||||
}
|
||||
|
||||
$comics = $this->copyManga->comics(30, $request->header('offset', 0), $request->get('top', 'all'), $params);
|
||||
|
||||
// Prep the array for upsert
|
||||
$comicsUpsertArray = [];
|
||||
$authorsUpsertArray = [];
|
||||
|
||||
foreach ($comics['list'] as $comic) {
|
||||
$comicsUpsertArray[] = [
|
||||
'pathword' => $comic['path_word'],
|
||||
'uuid' => '',
|
||||
'name' => $comic['name'],
|
||||
'alias' => '{}',
|
||||
'description' => '',
|
||||
'cover' => $comic['cover'],
|
||||
'upstream_updated_at' => $comic['datetime_updated'],
|
||||
];
|
||||
|
||||
foreach ($comic['author'] as $author) {
|
||||
$authorsUpsertArray[] = [
|
||||
'name' => $author['name']
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Do an upsert for comics
|
||||
Comic::upsert($comicsUpsertArray, uniqueBy: 'pathword', update: ['upstream_updated_at']);
|
||||
Author::upsert($authorsUpsertArray, uniqueBy: 'name');
|
||||
|
||||
// Had to do a second pass to insert the relationships
|
||||
foreach ($comics['list'] as $comic) {
|
||||
// Get the comic id
|
||||
$comicObj = Comic::where('pathword', $comic['path_word'])->first();
|
||||
|
||||
foreach ($comic['author'] as $author) {
|
||||
$authorObj = Author::where('name', $author['name'])->first();
|
||||
$comicObj->authors()->sync($authorObj);
|
||||
}
|
||||
}
|
||||
|
||||
return Inertia::render('Comic/Index', [
|
||||
'comics' => $comics,
|
||||
'offset' => $request->header('offset', 0)
|
||||
]);
|
||||
}
|
||||
|
||||
public function chapters(Request $request, string $pathword = ''): Response
|
||||
{
|
||||
$comic = $this->copyManga->comic($pathword);
|
||||
$chapters = $this->copyManga->chapters($pathword, 200, 0, [], $request->get('group', 'default'));
|
||||
|
||||
// Get the comic object and fill other parameters
|
||||
$comicObject = Comic::where('pathword', $pathword)->first();
|
||||
$comicObject->uuid = $comic['comic']['uuid'];
|
||||
$comicObject->alias = explode(',', $comic['comic']['alias']);
|
||||
$comicObject->description = $comic['comic']['brief'];
|
||||
$comicObject->metadata = $comic;
|
||||
$comicObject->save();
|
||||
|
||||
// Get the authors and update the pathword
|
||||
foreach ($comic['comic']['author'] as $author) {
|
||||
$authorObj = Author::where('name', $author['name'])->whereNull('pathword')->first();
|
||||
|
||||
if ($authorObj) {
|
||||
// Do nothing if pathword already exist
|
||||
$authorObj->pathword = $author['path_word'];
|
||||
$authorObj->save();
|
||||
}
|
||||
}
|
||||
|
||||
// Do the Chapters
|
||||
// Prep the array for upsert
|
||||
$arrayForUpsert = [];
|
||||
foreach ($chapters['list'] as $chapter) {
|
||||
$arrayForUpsert[] = [
|
||||
'comic_id' => $comicObject->id,
|
||||
'chapter_uuid' => $chapter['uuid'],
|
||||
'name' => $chapter['name'],
|
||||
'order' => $chapter['index'],
|
||||
'upstream_created_at' => $chapter['datetime_created'],
|
||||
'metadata' => json_encode($chapter),
|
||||
];
|
||||
}
|
||||
|
||||
// Do an upsert
|
||||
Chapter::upsert($arrayForUpsert, uniqueBy: 'chapter_uuid');
|
||||
|
||||
return Inertia::render('Comic/Chapters', [
|
||||
'comic' => $comic,
|
||||
'chapters' => $chapters,
|
||||
]);
|
||||
}
|
||||
|
||||
public function read(Request $request, string $pathword = '', string $uuid = ''): Response
|
||||
{
|
||||
$comic = $this->copyManga->comic($pathword);
|
||||
$chapter = $this->copyManga->chapter($pathword, $uuid);
|
||||
|
||||
// Get the authors and update the pathword
|
||||
foreach ($comic['comic']['author'] as $author) {
|
||||
$authorObj = Author::where('name', $author['name'])->whereNull('pathword')->first();
|
||||
|
||||
if ($authorObj) {
|
||||
// Do nothing if pathword already exist
|
||||
$authorObj->pathword = $author['path_word'];
|
||||
$authorObj->save();
|
||||
}
|
||||
}
|
||||
|
||||
$chapterObj = Chapter::where('chapter_uuid', $chapter['chapter']['uuid'])->first();
|
||||
$comicObj = Comic::where('pathword', $pathword)->first();
|
||||
|
||||
$arrayForUpsert = [];
|
||||
|
||||
foreach ($chapter['sorted'] as $k => $image) {
|
||||
$metadata = $chapter;
|
||||
unset($metadata['sorted']);
|
||||
unset($metadata['chapter']['contents'], $metadata['chapter']['words']);
|
||||
|
||||
$arrayForUpsert[] = [
|
||||
'comic_id' => $comicObj->id,
|
||||
'chapter_id' => $chapterObj->id,
|
||||
'order' => $k,
|
||||
'url' => $image['url'],
|
||||
'metadata' => json_encode($metadata),
|
||||
];
|
||||
}
|
||||
|
||||
// Do an upsert
|
||||
Image::upsert($arrayForUpsert, uniqueBy: 'url');
|
||||
|
||||
return Inertia::render('Comic/Read', [
|
||||
'comic' => $comic,
|
||||
'chapter' => $chapter,
|
||||
]);
|
||||
}
|
||||
|
||||
public function tags()
|
||||
{
|
||||
// TODO
|
||||
$tags = $this->copyManga->tags();
|
||||
Cache::forever('tags', $tags);
|
||||
|
||||
return response()->json($tags);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Http\Resources\UserCollection;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Inertia\Middleware;
|
||||
|
||||
class HandleInertiaRequests extends Middleware
|
||||
@@ -31,8 +33,9 @@ class HandleInertiaRequests extends Middleware
|
||||
{
|
||||
return [
|
||||
...parent::share($request),
|
||||
'tags' => Cache::get('tags'),
|
||||
'auth' => [
|
||||
'user' => $request->user(),
|
||||
'user' => $request->user() ? (new UserCollection($request->user()))->toArray($request) : null,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Requests\Auth;
|
||||
|
||||
use Illuminate\Auth\Events\Lockout;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
@@ -22,7 +23,7 @@ class LoginRequest extends FormRequest
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
@@ -35,7 +36,7 @@ class LoginRequest extends FormRequest
|
||||
/**
|
||||
* Attempt to authenticate the request's credentials.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function authenticate(): void
|
||||
{
|
||||
@@ -55,7 +56,7 @@ class LoginRequest extends FormRequest
|
||||
/**
|
||||
* Ensure the login request is not rate limited.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function ensureIsNotRateLimited(): void
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
@@ -11,7 +12,7 @@ class ProfileUpdateRequest extends FormRequest
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
@@ -19,6 +20,7 @@ class ProfileUpdateRequest extends FormRequest
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => [
|
||||
'required',
|
||||
'contains:@yumj.in',
|
||||
'string',
|
||||
'lowercase',
|
||||
'email',
|
||||
|
||||
24
app/Http/Resources/UserCollection.php
Normal file
24
app/Http/Resources/UserCollection.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class UserCollection extends ResourceCollection
|
||||
{
|
||||
/**
|
||||
* Transform the resource collection into an array.
|
||||
*
|
||||
* @return array<int|string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $request->user()->id,
|
||||
'name' => $request->user()->name,
|
||||
'email' => $request->user()->email,
|
||||
'favourites' => $request->user()->favourites()->get(['pathword'])->pluck('pathword')
|
||||
];
|
||||
}
|
||||
}
|
||||
21
app/Models/Author.php
Normal file
21
app/Models/Author.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
|
||||
class Author extends Model
|
||||
{
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'pathword',
|
||||
];
|
||||
|
||||
public function comics(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Comic::class);
|
||||
}
|
||||
|
||||
}
|
||||
30
app/Models/Chapter.php
Normal file
30
app/Models/Chapter.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Chapter extends Model
|
||||
{
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'chapter_uuid' => 'string',
|
||||
'upstream_created_at' => 'date',
|
||||
'metadata' => 'json',
|
||||
];
|
||||
}
|
||||
|
||||
public function comic(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Comic::class);
|
||||
}
|
||||
|
||||
public function images(): HasMany
|
||||
{
|
||||
return $this->hasMany(Image::class);
|
||||
}
|
||||
|
||||
}
|
||||
31
app/Models/Comic.php
Normal file
31
app/Models/Comic.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Comic extends Model
|
||||
{
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'uuid' => 'string',
|
||||
'alias' => 'array',
|
||||
'upstream_updated_at' => 'datetime:Y-m-d',
|
||||
'metadata' => 'json',
|
||||
];
|
||||
}
|
||||
|
||||
public function authors(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Author::class);
|
||||
}
|
||||
|
||||
public function chapters(): HasMany
|
||||
{
|
||||
return $this->hasMany(Chapter::class);
|
||||
}
|
||||
|
||||
}
|
||||
36
app/Models/Image.php
Normal file
36
app/Models/Image.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Image extends Model
|
||||
{
|
||||
|
||||
protected $fillable = [
|
||||
'comic_id',
|
||||
'chapter_id',
|
||||
'order',
|
||||
'url',
|
||||
'metadata',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'metadata' => 'json',
|
||||
];
|
||||
}
|
||||
|
||||
public function chapter(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Chapter::class);
|
||||
}
|
||||
|
||||
public function comic(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Comic::class);
|
||||
}
|
||||
|
||||
}
|
||||
26
app/Models/ReadingHistory.php
Normal file
26
app/Models/ReadingHistory.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ReadingHistory extends Model
|
||||
{
|
||||
|
||||
public function comic(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Comic::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function chapter(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Chapter::class);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,14 +2,17 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Database\Factories\UserFactory;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
class User extends Authenticatable
|
||||
class User extends Authenticatable implements MustVerifyEmail
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
@@ -45,4 +48,15 @@ class User extends Authenticatable
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
|
||||
public function favourites(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Comic::class, 'user_favourite')->withTimestamps();
|
||||
}
|
||||
|
||||
public function readingHistories(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Comic::class, 'reading_histories')->withTimestamps();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
336
app/Remote/CopyManga.php
Normal file
336
app/Remote/CopyManga.php
Normal file
@@ -0,0 +1,336 @@
|
||||
<?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;
|
||||
return $this->execute($this->buildUrl("comics", $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;
|
||||
}
|
||||
|
||||
}
|
||||
86
app/Remote/ImageFetcher.php
Normal file
86
app/Remote/ImageFetcher.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Remote;
|
||||
|
||||
use App\Enum\MimeType;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class ImageFetcher
|
||||
{
|
||||
|
||||
private array $options = [
|
||||
'cache' => [
|
||||
'enabled' => true,
|
||||
'prefix' => 'image_fetcher_',
|
||||
'ttl' => 3600,
|
||||
],
|
||||
'http' => [
|
||||
'timeout' => 60,
|
||||
'userAgent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
|
||||
]
|
||||
];
|
||||
|
||||
public function __construct(private readonly string $url = "")
|
||||
{
|
||||
// Global useragent
|
||||
Http::globalRequestMiddleware(fn ($request) => $request->withHeader(
|
||||
'User-Agent', $this->options['http']['userAgent']
|
||||
));
|
||||
}
|
||||
|
||||
public function forget(): bool
|
||||
{
|
||||
if (!$this->options['cache']['enabled']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Cache::forget($this->cacheName($this->url));
|
||||
}
|
||||
|
||||
public function getExtension(): string
|
||||
{
|
||||
$path = parse_url($this->url, PHP_URL_PATH);
|
||||
return pathinfo($path, PATHINFO_EXTENSION);
|
||||
}
|
||||
|
||||
public function getMimeType(): MimeType
|
||||
{
|
||||
return MimeType::{$this->getExtension()};
|
||||
}
|
||||
|
||||
private function cacheName(string $url): string
|
||||
{
|
||||
return $this->options['cache']['prefix'] . base64_encode($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
protected function isCached(): bool
|
||||
{
|
||||
return $this->options['cache']['enabled'] && Cache::has($this->cacheName($this->url));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ConnectionException
|
||||
*/
|
||||
public function fetch()
|
||||
{
|
||||
if ($this->isCached()) {
|
||||
return Cache::get($this->cacheName($this->url));
|
||||
}
|
||||
|
||||
$response = Http::timeout($this->options['http']['timeout'])->get($this->url);
|
||||
$image = $response->body();
|
||||
|
||||
// Write to cache
|
||||
if ($this->options['cache']['enabled']) {
|
||||
Cache::add($this->cacheName($this->url), $image, $this->options['cache']['ttl']);
|
||||
}
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user