472 lines
16 KiB
PHP
472 lines
16 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Helper\ZhConversion;
|
|
use App\Jobs\ComicUpsert;
|
|
use App\Jobs\ImageUpsert;
|
|
use App\Jobs\RemotePrefetch;
|
|
use App\Models\Author;
|
|
use App\Models\Chapter;
|
|
use App\Models\Comic;
|
|
use App\Models\ReadingHistory;
|
|
use App\Remote\CopyManga;
|
|
use App\Remote\ImageFetcher;
|
|
use GuzzleHttp\Exception\GuzzleException;
|
|
use Illuminate\Contracts\Routing\ResponseFactory;
|
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
|
use Illuminate\Foundation\Application;
|
|
use Illuminate\Http\Client\ConnectionException;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Response as IlluminateHttpResponse;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Inertia\Inertia;
|
|
use Inertia\Response;
|
|
|
|
/**
|
|
* Comic Controller
|
|
*/
|
|
class ComicController extends Controller
|
|
{
|
|
|
|
/**
|
|
* Autoload classes
|
|
*
|
|
* @param CopyManga $copyManga
|
|
* @param ZhConversion $zhConversion
|
|
*/
|
|
public function __construct(
|
|
private readonly CopyManga $copyManga,
|
|
private readonly ZhConversion $zhConversion
|
|
) {}
|
|
|
|
/**
|
|
* Convert SC to ZH
|
|
*
|
|
* @param mixed $string
|
|
* @return mixed
|
|
*/
|
|
protected function scToZh(mixed $string): mixed
|
|
{
|
|
if (gettype($string) !== 'string') {
|
|
$string = serialize($string);
|
|
return unserialize(str_replace(array_keys($this->zhConversion::ZH_TO_HANT), array_values($this->zhConversion::ZH_TO_HANT), $string));
|
|
}
|
|
|
|
return str_replace(array_keys($this->zhConversion::ZH_TO_HANT), array_values($this->zhConversion::ZH_TO_HANT), $string);
|
|
}
|
|
|
|
/**
|
|
* Show user favourites
|
|
*
|
|
* @param Request $request
|
|
* @return Response
|
|
*/
|
|
public function favourites(Request $request): Response
|
|
{
|
|
return Inertia::render('Comic/Favourites', [
|
|
'favourites' => $this->scToZh($request->user()->favourites()->with(['authors'])->orderBy('upstream_updated_at', 'desc')->get()),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Toggle user favourites
|
|
*
|
|
* @param Request $request
|
|
* @return JsonResponse
|
|
* @throws GuzzleException
|
|
*/
|
|
public function postFavourite(Request $request): JsonResponse
|
|
{
|
|
try {
|
|
// Get pathname to comic_id, if metadata is null, also do fetching
|
|
$comic = Comic::where('pathword', $request->pathword)->whereNotNull('metadata')->firstOrFail();
|
|
} catch (ModelNotFoundException $e) {
|
|
// Fetch from remote
|
|
$remoteComic = $this->copyManga->comic($request->pathword);
|
|
|
|
$comic = Comic::create([
|
|
'pathword' => $remoteComic['comic']['path_word'],
|
|
'name' => $remoteComic['comic']['name'],
|
|
'cover' => $remoteComic['comic']['cover'],
|
|
'upstream_updated_at' => $remoteComic['comic']['datetime_updated'],
|
|
'uuid' => $remoteComic['comic']['uuid'],
|
|
'alias' => explode(',', $remoteComic['comic']['alias']),
|
|
'description' => $remoteComic['comic']['brief'],
|
|
'metadata' => $remoteComic,
|
|
]);
|
|
}
|
|
|
|
// 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'));
|
|
}
|
|
|
|
/**
|
|
* Show image via proxy
|
|
*
|
|
* @param Request $request
|
|
* @param string $url
|
|
* @return ResponseFactory|Application|IlluminateHttpResponse
|
|
* @throws ConnectionException
|
|
*/
|
|
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',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Index / Tags for comic listing
|
|
*
|
|
* @param Request $request
|
|
* @return Response
|
|
* @throws GuzzleException
|
|
*/
|
|
public function index(Request $request): Response
|
|
{
|
|
$params = [];
|
|
if ($request->has('tag')) {
|
|
$params['theme'] = $request->get('tag');
|
|
}
|
|
|
|
$offset = $request->header('offset', 0);
|
|
$comics = $this->copyManga->comics(30, $offset, $request->get('top', 'all'), $params);
|
|
|
|
// Upsert into DB
|
|
ComicUpsert::dispatch($comics);
|
|
|
|
// Prefetch next page
|
|
RemotePrefetch::dispatch('comics', [
|
|
'limit' => 30,
|
|
'offset' => $offset + 30,
|
|
'top' => $request->get('top', 'all'),
|
|
'params' => $params
|
|
])->delay(now()->addSecond());
|
|
|
|
return Inertia::render('Comic/Index', [
|
|
'comics' => $this->scToZh($comics),
|
|
'offset' => $offset
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Author for comic listing
|
|
*
|
|
* @param Request $request
|
|
* @param string $author
|
|
* @return Response
|
|
* @throws GuzzleException
|
|
*/
|
|
public function author(Request $request, string $author): Response
|
|
{
|
|
$offset = $request->header('offset', 0);
|
|
$comics = $this->copyManga->comics(30, $offset, $request->get('top', 'all'), ['author' => $author]);
|
|
|
|
// Upsert into DB
|
|
ComicUpsert::dispatch($comics);
|
|
|
|
return Inertia::render('Comic/Index', [
|
|
'comics' => $this->scToZh($comics),
|
|
'offset' => $offset
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Search for comic listing
|
|
*
|
|
* @param Request $request
|
|
* @param string $search
|
|
* @return Response
|
|
* @throws GuzzleException
|
|
*/
|
|
public function search(Request $request, string $search): Response
|
|
{
|
|
$offset = $request->header('offset', 0);
|
|
$comics = $this->copyManga->search($search, 30, $offset);
|
|
|
|
// Search API is limited, no upsert
|
|
|
|
return Inertia::render('Comic/Index', [
|
|
'comics' => $this->scToZh($comics),
|
|
'offset' => $offset
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Show comic details and chapters
|
|
*
|
|
* @param Request $request
|
|
* @param string $pathword
|
|
* @return RedirectResponse|Response
|
|
* @throws GuzzleException
|
|
*/
|
|
public function chapters(Request $request, string $pathword = ''): RedirectResponse|Response
|
|
{
|
|
if ($pathword === 'installHook.js.map') {
|
|
return to_route('comics.index');
|
|
}
|
|
|
|
$offset = $request->header('offset', 0);
|
|
|
|
$comic = $this->copyManga->comic($pathword);
|
|
$chapters = $this->copyManga->chapters($pathword, 200, $offset, [], $request->get('group', 'default'), $request->get('reload', false));
|
|
|
|
// Get the comic object and fill other parameters
|
|
try {
|
|
$comicObject = Comic::where('pathword', $pathword)->firstOrFail();
|
|
$comicObject->uuid = $comic['comic']['uuid'];
|
|
$comicObject->alias = explode(',', $comic['comic']['alias']);
|
|
$comicObject->description = $comic['comic']['brief'];
|
|
$comicObject->metadata = $comic;
|
|
$comicObject->save();
|
|
} catch (ModelNotFoundException $e) {
|
|
$comicObject = Comic::create([
|
|
'name' => $comic['comic']['name'],
|
|
'pathword' => $comic['comic']['path_word'],
|
|
'cover' => $comic['comic']['cover'],
|
|
'upstream_updated_at' => $comic['comic']['datetime_updated'],
|
|
'uuid' => $comic['comic']['uuid'],
|
|
'alias' => explode(',', $comic['comic']['alias']),
|
|
'description' => $comic['comic']['brief'],
|
|
'metadata' => $comic
|
|
]);
|
|
}
|
|
|
|
// 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');
|
|
|
|
// Get user history
|
|
$histories = $request->user()->readingHistories()->where('reading_histories.comic_id', $comicObject->id)
|
|
->distinct()->select('chapter_uuid')->get()->pluck('chapter_uuid');
|
|
|
|
// Check if the first chapter is already fetched or not, if not, do a prefetch
|
|
if (!$histories->contains($chapters['list'][0]['uuid'])) {
|
|
RemotePrefetch::dispatch('chapter', ['pathword' => $pathword, 'uuid' => $chapters['list'][0]['uuid']]);
|
|
}
|
|
|
|
return Inertia::render('Comic/Chapters', [
|
|
'comic' => $this->scToZh($comic),
|
|
'chapters' => $this->scToZh($chapters),
|
|
'histories' => $histories,
|
|
'offset' => $offset
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Read a chapter, showing images
|
|
*
|
|
* @param Request $request
|
|
* @param string $pathword
|
|
* @param string $uuid
|
|
* @return Response
|
|
* @throws GuzzleException
|
|
*/
|
|
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();
|
|
|
|
// Image Upsert
|
|
ImageUpsert::dispatch($comicObj->id, $chapterObj->id, $chapter);
|
|
|
|
// Update history
|
|
$request->user()->readingHistories()->attach($chapterObj->id, ['comic_id' => $comicObj->id]);
|
|
|
|
// Get chapters from DB
|
|
$chapters = $comicObj->chapters()->where('metadata->group_path_word', $chapter['chapter']['group_path_word'])->orderBy('order')->get(['name', 'chapter_uuid']);
|
|
|
|
// Do remote prefetch if needed
|
|
if ($chapter['chapter']['next'] !== null) {
|
|
try {
|
|
Chapter::where('chapter_uuid', $chapter['chapter']['next'])->firstOrFail()->images()->firstOrFail();
|
|
} catch (ModelNotFoundException $e) {
|
|
RemotePrefetch::dispatch('chapter', ['pathword' => $pathword, 'uuid' => $chapter['chapter']['next']]);
|
|
}
|
|
}
|
|
|
|
return Inertia::render('Comic/Read', [
|
|
'comic' => $this->scToZh($comic),
|
|
'chapter' => $this->scToZh($chapter),
|
|
'chapters' => $this->scToZh($chapters),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Show user read histories
|
|
*
|
|
* @param Request $request
|
|
* @return Response
|
|
*/
|
|
public function histories(Request $request): Response
|
|
{
|
|
// Group by comic
|
|
if ($request->get('group', '') === 'comic') {
|
|
$readingHistory = ReadingHistory::query()
|
|
->join('comics', 'reading_histories.comic_id', '=', 'comics.id')
|
|
->join('chapters', 'reading_histories.chapter_id', '=', 'chapters.id')
|
|
->select(
|
|
'comics.id as comic_id',
|
|
'comics.name as comic_name',
|
|
'comics.pathword as comic_pathword',
|
|
'comics.upstream_updated_at as comic_upstream_updated_at',
|
|
'chapters.name as chapter_name',
|
|
'chapters.chapter_uuid as chapter_uuid',
|
|
'reading_histories.created_at as read_at'
|
|
)
|
|
->where('reading_histories.user_id', $request->user()->id)
|
|
->orderBy('comics.name')->orderByDesc('reading_histories.created_at')->paginate(2000);
|
|
|
|
// Transform the paginated data into a 2D array grouped by comic_id, including comic data and histories
|
|
$groupedData = $readingHistory->getCollection()
|
|
->groupBy('comic_id')->map(function (Collection $records, $comicId) {
|
|
$firstRecord = $records->first();
|
|
return [
|
|
'comic' => [
|
|
'comic_id' => $comicId,
|
|
'comic_name' => $firstRecord->comic_name,
|
|
'comic_pathword' => $firstRecord->comic_pathword,
|
|
'comic_upstream_updated_at' => $firstRecord->comic_upstream_updated_at,
|
|
],
|
|
'histories' => $records->map(fn($record) => [
|
|
'chapter_name' => $record->chapter_name,
|
|
'chapter_uuid' => $record->chapter_uuid,
|
|
'read_at' => $record->read_at,
|
|
])->toArray(),
|
|
];
|
|
})->values()->toArray(); // Use values() to reset numeric keys after grouping
|
|
|
|
return Inertia::render('Comic/HistoriesByComic', [
|
|
'histories' => $this->scToZh($groupedData)
|
|
]);
|
|
}
|
|
|
|
// Only order by chapter
|
|
$histories = $request->user()->readingHistories()->with(['comic:id,name,pathword'])->orderByDesc('reading_histories.created_at')
|
|
->select(['reading_histories.id as hid', 'chapters.comic_id', 'chapters.name'])->paginate(50)->toArray();
|
|
|
|
return Inertia::render('Comic/Histories', [
|
|
'histories' => $this->scToZh($histories)
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Delete user read histories by submitting ids
|
|
*
|
|
* @param Request $request
|
|
* @return RedirectResponse
|
|
*/
|
|
public function patchHistories(Request $request): RedirectResponse
|
|
{
|
|
if (!is_array($request->get('ids')) && $request->get('ids') === 'all') {
|
|
$histories = $request->user()->readingHistories()->delete();
|
|
} else {
|
|
$histories = $request->user()->readingHistories()->whereIn('reading_histories.id', $request->get('ids'))->delete();
|
|
}
|
|
|
|
return redirect()->route('comics.histories');
|
|
}
|
|
|
|
/**
|
|
* Remove histories for specified comic
|
|
*
|
|
* @param Request $request
|
|
* @param string $pathword
|
|
* @return Response
|
|
*/
|
|
public function destroyHistory(Request $request, string $pathword): Response
|
|
{
|
|
$comicId = Comic::where('pathword', $pathword)->firstOrFail(['id'])->id;
|
|
$request->user()->readingHistories()->where('reading_histories.comic_id', $comicId)->delete();
|
|
|
|
// Get history
|
|
$histories = $request->user()->readingHistories()->where('reading_histories.comic_id', $comicId)
|
|
->distinct()->select('chapter_uuid')->get()->pluck('chapter_uuid');
|
|
|
|
return Inertia::render('Comic/Chapters', [
|
|
'histories' => $histories
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Remove duplicated records
|
|
*
|
|
* @param Request $request
|
|
* @return RedirectResponse
|
|
*/
|
|
public function destroyHistories(Request $request): RedirectResponse
|
|
{
|
|
$request->user()->cleanUpReadingHistories();
|
|
|
|
return redirect()->route('comics.histories');
|
|
}
|
|
|
|
/**
|
|
* Fetch tags
|
|
*
|
|
* @return JsonResponse
|
|
* @throws GuzzleException
|
|
*/
|
|
public function tags()
|
|
{
|
|
// TODO
|
|
$tags = $this->scToZh($this->copyManga->tags());
|
|
|
|
Cache::forever('tags', $tags);
|
|
|
|
return response()->json($tags);
|
|
}
|
|
}
|