feat(admin): rebuild release upload form with Livewire and CDN validation
Replace the server-rendered release upload form with a Livewire component, introducing an interactive admin release form that supports dynamic episode management, real‑time CDN path validation, and direct download file checks. Key changes: - New `AdminReleaseForm` Livewire component with dynamic episode fields, Tagify integration, and upload handling via Livewire's file uploads. - Added `CdnPathValidator` service to verify download and stream paths across all configured mirrors, caching results and capturing file sizes. - Extended `EpisodeService` and `GalleryService` to support array‑based data and temporary uploaded files from Livewire. - Persist validated download sizes and store the validation timestamp on the `downloads` table via a new migration; also add a unique constraint on `hentais.slug` to prevent duplicates. - Remove the old POST `/admin/release/upload` route and controller logic, along with the legacy `upload.js` script. - Add comprehensive feature and unit tests covering the new component and the CDN validation service.
This commit is contained in:
@@ -3,33 +3,10 @@
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\DiscordReleaseNotification;
|
||||
use App\Models\Hentai;
|
||||
use App\Services\DownloadService;
|
||||
use App\Services\EpisodeService;
|
||||
use App\Services\GalleryService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ReleaseController extends Controller
|
||||
{
|
||||
protected EpisodeService $episodeService;
|
||||
|
||||
protected GalleryService $galleryService;
|
||||
|
||||
protected DownloadService $downloadService;
|
||||
|
||||
public function __construct(
|
||||
EpisodeService $episodeService,
|
||||
GalleryService $galleryService,
|
||||
DownloadService $downloadService
|
||||
) {
|
||||
$this->episodeService = $episodeService;
|
||||
$this->galleryService = $galleryService;
|
||||
$this->downloadService = $downloadService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display release page
|
||||
*/
|
||||
@@ -37,55 +14,4 @@ class ReleaseController extends Controller
|
||||
{
|
||||
return view('admin.release.create');
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload New Hentai with One or Multipe Episodes
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
// Create new Hentai or find existing one
|
||||
$slug = $this->episodeService->generateSlug($request->input('title'));
|
||||
|
||||
$hentai = Hentai::where('slug', $slug)->first();
|
||||
|
||||
// If hentai exists and was created today, return to home
|
||||
if ($hentai?->created_at->isToday()) {
|
||||
return to_route('home.index');
|
||||
}
|
||||
|
||||
// If hentai does not exist, create a new instance
|
||||
$hentai = Hentai::firstOrCreate(
|
||||
['slug' => $slug],
|
||||
['description' => $request->input('description1')]
|
||||
);
|
||||
|
||||
// Studio
|
||||
$studio = $this->episodeService->getOrCreateStudio(json_decode($request->input('studio'))[0]->value);
|
||||
|
||||
// Create Episode(s)
|
||||
$releasedEpisodes = [];
|
||||
for ($i = 1; $i <= $request->input('episodes'); $i++) {
|
||||
|
||||
$episode = $this->episodeService->createEpisode($request, $hentai, $i, $studio);
|
||||
|
||||
$this->episodeService->createOrUpdateCover($request, $episode, $slug, $i);
|
||||
$this->downloadService->createOrUpdateDownloads($request, $episode, $i);
|
||||
$this->galleryService->createOrUpdateGallery($request, $hentai, $episode, $i);
|
||||
|
||||
$releasedEpisodes[] = $episode->slug;
|
||||
}
|
||||
|
||||
if ($request->has('censored')) {
|
||||
DiscordReleaseNotification::dispatch($request->input('title'), 'release-censored');
|
||||
} else {
|
||||
foreach ($releasedEpisodes as $slug) {
|
||||
// Dispatch Discord Alert
|
||||
DiscordReleaseNotification::dispatch($slug, 'release');
|
||||
}
|
||||
}
|
||||
|
||||
cache()->flush();
|
||||
|
||||
return to_route('home.index');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Jobs\DiscordReleaseNotification;
|
||||
use App\Models\Hentai;
|
||||
use App\Services\CdnPathValidator;
|
||||
use App\Services\DownloadService;
|
||||
use App\Services\EpisodeService;
|
||||
use App\Services\GalleryService;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Livewire\Component;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
use Livewire\WithFileUploads;
|
||||
|
||||
class AdminReleaseForm extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
public string $title = '';
|
||||
|
||||
public string $titleJpn = '';
|
||||
|
||||
public string $studio = '';
|
||||
|
||||
/** @var string[] */
|
||||
public array $tags = [];
|
||||
|
||||
public string $releasedate = '';
|
||||
|
||||
public string $baseurl = '';
|
||||
|
||||
public bool $censored = false;
|
||||
|
||||
public int $episodeCount = 1;
|
||||
|
||||
/** @var array<int, array{description: string, cover: TemporaryUploadedFile|null, gallery: TemporaryUploadedFile[], downloads: array{fhd: string, fhdi: string, uhd: string, uhdi: string}}> */
|
||||
public array $episodes = [];
|
||||
|
||||
/** @var array<string, array{state: string, message: string, mirrors: array}> */
|
||||
public array $validation = [];
|
||||
|
||||
public bool $overrideValidation = false;
|
||||
|
||||
public bool $saving = false;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->episodes = [$this->defaultEpisode()];
|
||||
}
|
||||
|
||||
private function defaultEpisode(): array
|
||||
{
|
||||
return [
|
||||
'description' => '',
|
||||
'cover' => null,
|
||||
'gallery' => [],
|
||||
'downloads' => ['fhd' => '', 'fhdi' => '', 'uhd' => '', 'uhdi' => ''],
|
||||
];
|
||||
}
|
||||
|
||||
public function addEpisode(): void
|
||||
{
|
||||
$this->episodes[] = $this->defaultEpisode();
|
||||
$this->episodeCount = count($this->episodes);
|
||||
|
||||
if (trim($this->baseurl) !== '') {
|
||||
$this->validateStream();
|
||||
}
|
||||
}
|
||||
|
||||
public function removeEpisode(int $index): void
|
||||
{
|
||||
if (count($this->episodes) <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
unset($this->episodes[$index]);
|
||||
$this->episodes = array_values($this->episodes);
|
||||
$this->episodeCount = count($this->episodes);
|
||||
$this->pruneValidation();
|
||||
|
||||
if (trim($this->baseurl) !== '') {
|
||||
$this->validateStream();
|
||||
}
|
||||
}
|
||||
|
||||
public function getHasInvalidValidationProperty(): bool
|
||||
{
|
||||
foreach ($this->validation as $status) {
|
||||
if (($status['state'] ?? '') === 'invalid') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function updatedEpisodes(mixed $value, string $key): void
|
||||
{
|
||||
if (preg_match('/^(\d+)\.downloads\.(fhd|fhdi|uhd|uhdi)$/', $key, $matches)) {
|
||||
$this->validateDownload((int) $matches[1], $matches[2]);
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedBaseurl(): void
|
||||
{
|
||||
$this->validateStream();
|
||||
}
|
||||
|
||||
public function validateDownload(int $episodeIndex, string $quality): void
|
||||
{
|
||||
$key = "episodes.{$episodeIndex}.downloads.{$quality}";
|
||||
$url = $this->episodes[$episodeIndex]['downloads'][$quality] ?? '';
|
||||
|
||||
if (trim($url) === '') {
|
||||
$this->validation[$key] = ['state' => 'idle', 'message' => '', 'mirrors' => []];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->validation[$key] = ['state' => 'checking', 'message' => 'Checking mirrors…', 'mirrors' => []];
|
||||
|
||||
$result = app(CdnPathValidator::class)->validateDownload($this->qualityToType($quality), $url);
|
||||
|
||||
$this->validation[$key] = [
|
||||
'state' => $result['valid'] ? 'valid' : 'invalid',
|
||||
'message' => $result['valid'] ? 'Verified on all mirrors' : 'Missing on one or more mirrors',
|
||||
'mirrors' => $result['mirrors'],
|
||||
];
|
||||
}
|
||||
|
||||
public function validateStream(): void
|
||||
{
|
||||
$baseurl = trim($this->baseurl);
|
||||
|
||||
if ($baseurl === '') {
|
||||
$this->validation['stream'] = ['state' => 'idle', 'message' => '', 'mirrors' => []];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->validation['stream'] = ['state' => 'checking', 'message' => 'Checking mirrors…', 'mirrors' => []];
|
||||
|
||||
$result = app(CdnPathValidator::class)->validateStream($baseurl, count($this->episodes));
|
||||
|
||||
$this->validation['stream'] = [
|
||||
'state' => $result['valid'] ? 'valid' : 'invalid',
|
||||
'message' => $result['valid'] ? 'Verified on all mirrors' : 'Missing on one or more mirrors',
|
||||
'mirrors' => $result['mirrors'],
|
||||
];
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$this->validate([
|
||||
'title' => 'required|string|max:255',
|
||||
'titleJpn' => 'required|string|max:255',
|
||||
'studio' => 'required|string|max:255',
|
||||
'tags' => 'required|array|min:1',
|
||||
'releasedate' => 'required|date',
|
||||
'baseurl' => ['required', 'string', 'regex:/^[A-Za-z0-9_.\-]+\/[A-Za-z0-9_.\-]+/'],
|
||||
'episodes.*.description' => 'required|string',
|
||||
'episodes.*.cover' => 'required|image|max:20480',
|
||||
'episodes.*.gallery.*' => 'nullable|image|max:20480',
|
||||
'episodes.*.downloads.fhd' => 'required|string',
|
||||
'episodes.*.downloads.uhd' => 'required|string',
|
||||
]);
|
||||
|
||||
$this->saving = true;
|
||||
|
||||
// 1) Re-validate every non-empty download path + stream against the CDN.
|
||||
$validator = app(CdnPathValidator::class);
|
||||
$sizes = [];
|
||||
|
||||
foreach ($this->episodes as $index => $episode) {
|
||||
foreach ($episode['downloads'] as $quality => $url) {
|
||||
if (trim($url) === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = $this->qualityToType($quality);
|
||||
$key = "episodes.{$index}.downloads.{$quality}";
|
||||
$result = $validator->validateDownload($type, $url, true);
|
||||
|
||||
$this->validation[$key] = [
|
||||
'state' => $result['valid'] ? 'valid' : 'invalid',
|
||||
'message' => $result['valid'] ? 'Verified on all mirrors' : 'Missing on one or more mirrors',
|
||||
'mirrors' => $result['mirrors'],
|
||||
];
|
||||
$sizes[$index][$type] = $result['size'];
|
||||
|
||||
if (! $result['valid'] && ! $this->overrideValidation) {
|
||||
$this->addError($key, 'Path not found on all CDN mirrors.');
|
||||
$this->saving = false;
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (trim($this->baseurl) !== '') {
|
||||
$result = $validator->validateStream($this->baseurl, count($this->episodes), true);
|
||||
|
||||
$this->validation['stream'] = [
|
||||
'state' => $result['valid'] ? 'valid' : 'invalid',
|
||||
'message' => $result['valid'] ? 'Verified on all mirrors' : 'Missing on one or more mirrors',
|
||||
'mirrors' => $result['mirrors'],
|
||||
];
|
||||
|
||||
if (! $result['valid'] && ! $this->overrideValidation) {
|
||||
$this->addError('baseurl', 'Stream path not found on all CDN mirrors.');
|
||||
$this->saving = false;
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Duplicate-guard: block re-releasing a title created today.
|
||||
$episodeService = app(EpisodeService::class);
|
||||
$slug = $episodeService->generateSlug($this->title);
|
||||
|
||||
$existing = Hentai::where('slug', $slug)->first();
|
||||
if ($existing && $existing->created_at->isToday()) {
|
||||
$this->addError('title', 'A release with this title was already created today. Refine the title to avoid a duplicate.');
|
||||
$this->saving = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 3) Persist.
|
||||
$hentai = Hentai::firstOrCreate(
|
||||
['slug' => $slug],
|
||||
['description' => $this->episodes[0]['description']]
|
||||
);
|
||||
|
||||
$studio = $episodeService->getOrCreateStudio(trim($this->studio));
|
||||
|
||||
$galleryService = app(GalleryService::class);
|
||||
$downloadService = app(DownloadService::class);
|
||||
|
||||
$releasedEpisodes = [];
|
||||
foreach ($this->episodes as $index => $episode) {
|
||||
$episodeNumber = $index + 1;
|
||||
|
||||
$episodeModel = $episodeService->createEpisodeFromArray([
|
||||
'title' => $this->title,
|
||||
'title_jpn' => $this->titleJpn,
|
||||
'baseurl' => $this->baseurl,
|
||||
'description' => $episode['description'],
|
||||
'releasedate' => $this->releasedate,
|
||||
'tags' => $this->tags,
|
||||
'interpolated_uhd' => trim($episode['downloads']['uhdi'] ?? '') !== '',
|
||||
], $hentai, $episodeNumber, $studio);
|
||||
|
||||
if ($episode['cover']) {
|
||||
$episodeService->saveCoverFromFile($episodeModel, $hentai->slug, $episodeNumber, $episode['cover']);
|
||||
}
|
||||
|
||||
if (! empty($episode['gallery'])) {
|
||||
$galleryService->saveGalleryFiles($hentai, $episodeModel, $episodeNumber, $episode['gallery']);
|
||||
}
|
||||
|
||||
$downloads = [];
|
||||
foreach ($episode['downloads'] as $quality => $url) {
|
||||
if (trim($url) === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = $this->qualityToType($quality);
|
||||
$downloads[$type] = [
|
||||
'url' => trim($url),
|
||||
'size' => $sizes[$index][$type] ?? null,
|
||||
];
|
||||
}
|
||||
$downloadService->createOrUpdateDownloadsFromArray($episodeModel, $downloads);
|
||||
|
||||
$releasedEpisodes[] = $episodeModel->slug;
|
||||
}
|
||||
|
||||
// 4) Discord notifications + cache flush.
|
||||
if ($this->censored) {
|
||||
DiscordReleaseNotification::dispatch($this->title, 'release-censored');
|
||||
} else {
|
||||
foreach ($releasedEpisodes as $episodeSlug) {
|
||||
DiscordReleaseNotification::dispatch($episodeSlug, 'release');
|
||||
}
|
||||
}
|
||||
|
||||
cache()->flush();
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Failed to create release from Livewire form: '.$e->getMessage(), [
|
||||
'exception' => $e,
|
||||
'slug' => $slug,
|
||||
]);
|
||||
$this->saving = false;
|
||||
$this->addError('title', 'Something went wrong while saving. Please try again.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 5) Redirect.
|
||||
$this->redirectRoute('home.index');
|
||||
}
|
||||
|
||||
private function qualityToType(string $quality): string
|
||||
{
|
||||
return match ($quality) {
|
||||
'fhd' => 'FHD',
|
||||
'fhdi' => 'FHDi',
|
||||
'uhd' => 'UHD',
|
||||
'uhdi' => 'UHDi',
|
||||
};
|
||||
}
|
||||
|
||||
private function pruneValidation(): void
|
||||
{
|
||||
$count = count($this->episodes);
|
||||
foreach (array_keys($this->validation) as $key) {
|
||||
if (preg_match('/^episodes\.(\d+)\./', $key, $matches) && (int) $matches[1] >= $count) {
|
||||
unset($this->validation[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.admin-release-form');
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ class Downloads extends Model
|
||||
'episode_id',
|
||||
'type',
|
||||
'url',
|
||||
'size',
|
||||
'validated_at',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Http\Client\Response;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class CdnPathValidator
|
||||
{
|
||||
public const DOWNLOAD_PREFIX = [
|
||||
'FHD' => 'hentai-1080p/',
|
||||
'FHDi' => 'hentai-1080p/',
|
||||
'UHD' => 'hentai/',
|
||||
'UHDi' => 'hentai/',
|
||||
];
|
||||
|
||||
private const STREAM_PREFIX = 'hentai-stream/';
|
||||
|
||||
private const CACHE_TTL = 300;
|
||||
|
||||
public function validateDownload(string $type, string $url, bool $fresh = false): array
|
||||
{
|
||||
$path = self::DOWNLOAD_PREFIX[$type].ltrim($url, '/');
|
||||
$domains = config('hstream.download_domain_4k');
|
||||
|
||||
return $this->checkMirrors($domains, [$path], $fresh);
|
||||
}
|
||||
|
||||
public function validateStream(string $baseurl, int $episodeCount, bool $fresh = false): array
|
||||
{
|
||||
$domains = config('hstream.download_domain_4k');
|
||||
|
||||
// Validate the base directory, plus the first episode manifest as a sanity check.
|
||||
$paths = [self::STREAM_PREFIX.rtrim($baseurl, '/')];
|
||||
if ($episodeCount >= 1) {
|
||||
$paths[] = self::STREAM_PREFIX.rtrim($baseurl, '/').'/E'.str_pad(1, 2, '0', STR_PAD_LEFT).'/720/manifest.mpd';
|
||||
}
|
||||
|
||||
return $this->checkMirrors($domains, $paths, $fresh);
|
||||
}
|
||||
|
||||
private function checkMirrors(array $domains, array $paths, bool $fresh = false): array
|
||||
{
|
||||
$cacheKey = 'cdncheck:'.sha1(serialize([$domains, $paths]));
|
||||
|
||||
if ($fresh) {
|
||||
Cache::forget($cacheKey);
|
||||
}
|
||||
|
||||
return Cache::remember($cacheKey, self::CACHE_TTL, fn () => $this->runChecks($domains, $paths));
|
||||
}
|
||||
|
||||
private function runChecks(array $domains, array $paths): array
|
||||
{
|
||||
$entries = [];
|
||||
foreach ($domains as $domain) {
|
||||
foreach ($paths as $path) {
|
||||
$entries[] = ['domain' => $domain, 'path' => $path];
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$responses = Http::pool(function ($pool) use ($entries) {
|
||||
foreach ($entries as $entry) {
|
||||
$pool->timeout(4)
|
||||
->withHeaders(['Accept' => 'application/json'])
|
||||
->get($entry['domain'].'/check/'.$this->tokenize($entry['path']).'/'.$this->tokenize(now()->addHours(6)));
|
||||
}
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
$mirrors = [];
|
||||
foreach ($entries as $entry) {
|
||||
$mirrors[$entry['domain']][$entry['path']] = false;
|
||||
}
|
||||
|
||||
return [
|
||||
'valid' => false,
|
||||
'mirrors' => $mirrors,
|
||||
'size' => null,
|
||||
];
|
||||
}
|
||||
|
||||
$mirrors = [];
|
||||
$allValid = true;
|
||||
$firstSize = null;
|
||||
|
||||
foreach ($entries as $index => $entry) {
|
||||
$response = $responses[$index];
|
||||
$ok = $response instanceof Response
|
||||
&& $response->ok()
|
||||
&& $response->json('valid') === true;
|
||||
|
||||
$mirrors[$entry['domain']][$entry['path']] = $ok;
|
||||
|
||||
if (! $ok) {
|
||||
$allValid = false;
|
||||
} elseif ($firstSize === null) {
|
||||
$firstSize = $response->json('size');
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'valid' => $allValid,
|
||||
'mirrors' => $mirrors,
|
||||
'size' => $allValid ? $firstSize : null,
|
||||
];
|
||||
}
|
||||
|
||||
private function tokenize(string $value): string
|
||||
{
|
||||
return urlencode(Crypt::encryptString($value));
|
||||
}
|
||||
}
|
||||
@@ -33,4 +33,28 @@ class DownloadService
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist validated download URLs for an episode. Sizes are written
|
||||
* directly from the CDN validation response, so no size job is dispatched.
|
||||
*
|
||||
* @param array<string, array{url: string, size?: float|int|null}> $downloads
|
||||
*/
|
||||
public function createOrUpdateDownloadsFromArray(Episode $episode, array $downloads): void
|
||||
{
|
||||
foreach ($downloads as $type => $data) {
|
||||
if (empty($data['url'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Downloads::updateOrCreate([
|
||||
'episode_id' => $episode->id,
|
||||
'type' => $type,
|
||||
], [
|
||||
'url' => $data['url'],
|
||||
'size' => $data['size'] ?? null,
|
||||
'validated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,16 @@ namespace App\Services;
|
||||
|
||||
use App\Models\Episode;
|
||||
use App\Models\Hentai;
|
||||
use App\Models\Studios;
|
||||
use App\Models\ModLog;
|
||||
use App\Models\Studios;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Intervention\Image\Encoders\WebpEncoder;
|
||||
use Intervention\Image\Laravel\Facades\Image;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
|
||||
class EpisodeService
|
||||
{
|
||||
@@ -171,19 +173,60 @@ class EpisodeService
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an episode from an array of data (used by the Livewire release form).
|
||||
*
|
||||
* @param array{title: string, title_jpn: string, baseurl: string, description: string, releasedate: string, tags: string[], interpolated_uhd?: bool} $data
|
||||
*/
|
||||
public function createEpisodeFromArray(
|
||||
array $data,
|
||||
Hentai $hentai,
|
||||
int $episodeNumber,
|
||||
?Studios $studio = null
|
||||
): Episode {
|
||||
$episode = new Episode;
|
||||
$episode->title = $data['title'];
|
||||
$episode->title_search = preg_replace('/[^A-Za-z0-9 ]/', '', $episode->title);
|
||||
$episode->title_jpn = $data['title_jpn'];
|
||||
$episode->slug = "{$hentai->slug}-{$episodeNumber}";
|
||||
$episode->hentai_id = $hentai->id;
|
||||
$episode->studios_id = $studio->id;
|
||||
$episode->episode = $episodeNumber;
|
||||
$episode->description = $data['description'];
|
||||
$episode->url = rtrim($data['baseurl'], '/').'/E'.str_pad($episodeNumber, 2, '0', STR_PAD_LEFT);
|
||||
$episode->view_count = 0;
|
||||
$episode->interpolated = true;
|
||||
$episode->interpolated_uhd = $data['interpolated_uhd'] ?? false;
|
||||
$episode->is_dvd_aspect = false;
|
||||
$episode->release_date = Carbon::parse($data['releasedate'])->format('Y-m-d');
|
||||
$episode->cover_url = "/images/hentai/{$hentai->slug}/cover-ep-{$episodeNumber}.webp";
|
||||
$episode->save();
|
||||
|
||||
foreach ($data['tags'] as $tag) {
|
||||
$episode->tag($tag);
|
||||
}
|
||||
|
||||
return $episode;
|
||||
}
|
||||
|
||||
public function createOrUpdateCover(Request $request, Episode $episode, string $slug, int $episodeNumber): void
|
||||
{
|
||||
if (! $request->hasFile("episodecover{$episodeNumber}")) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->saveCoverFromFile($episode, $slug, $episodeNumber, $request->file("episodecover{$episodeNumber}"));
|
||||
}
|
||||
|
||||
public function saveCoverFromFile(Episode $episode, string $slug, int $episodeNumber, UploadedFile|TemporaryUploadedFile $file): void
|
||||
{
|
||||
// Create Folder for Image Upload
|
||||
if (! Storage::disk('public')->exists("/images/hentai/{$slug}")) {
|
||||
Storage::disk('public')->makeDirectory("/images/hentai/{$slug}");
|
||||
}
|
||||
|
||||
// Encode and save cover image
|
||||
Image::read($request->file("episodecover{$episodeNumber}")->getRealPath())
|
||||
Image::read($file->getRealPath())
|
||||
->cover(268, 394)
|
||||
->encode(new WebpEncoder)
|
||||
->save(Storage::disk('public')->path($episode->cover_url));
|
||||
|
||||
@@ -6,9 +6,11 @@ use App\Models\Episode;
|
||||
use App\Models\Gallery;
|
||||
use App\Models\Hentai;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Intervention\Image\Encoders\WebpEncoder;
|
||||
use Intervention\Image\Laravel\Facades\Image;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
|
||||
class GalleryService
|
||||
{
|
||||
@@ -17,17 +19,26 @@ class GalleryService
|
||||
$galleryInputNumber = $override ? 1 : $episodeNumber;
|
||||
|
||||
if ($request->hasFile('episodegallery'.$galleryInputNumber)) {
|
||||
$this->saveGalleryFiles($hentai, $episode, $episodeNumber, $request->file('episodegallery'.$galleryInputNumber));
|
||||
}
|
||||
}
|
||||
|
||||
$this->deleteOldGallery($episode);
|
||||
/**
|
||||
* Save a gallery for the given episode from a list of uploaded files.
|
||||
*
|
||||
* @param array<int, UploadedFile|TemporaryUploadedFile> $files
|
||||
*/
|
||||
public function saveGalleryFiles(Hentai $hentai, Episode $episode, int $episodeNumber, array $files): void
|
||||
{
|
||||
$this->deleteOldGallery($episode);
|
||||
|
||||
$this->createGalleryFolder($hentai);
|
||||
$this->createGalleryFolder($hentai);
|
||||
|
||||
$counter = 0;
|
||||
foreach ($request->file('episodegallery'.$galleryInputNumber) as $file) {
|
||||
$gallery = $this->createGallery($hentai, $episode, $episodeNumber, $counter);
|
||||
$this->saveGalleryImage($gallery, $file);
|
||||
$counter += 1;
|
||||
}
|
||||
$counter = 0;
|
||||
foreach ($files as $file) {
|
||||
$gallery = $this->createGallery($hentai, $episode, $episodeNumber, $counter);
|
||||
$this->saveGalleryImage($gallery, $file);
|
||||
$counter += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +62,7 @@ class GalleryService
|
||||
return $gallery;
|
||||
}
|
||||
|
||||
private function saveGalleryImage(Gallery $gallery, $sourceImage): void
|
||||
private function saveGalleryImage(Gallery $gallery, UploadedFile|TemporaryUploadedFile $sourceImage): void
|
||||
{
|
||||
Image::read($sourceImage->getRealPath())
|
||||
->cover(1920, 1080)
|
||||
|
||||
Reference in New Issue
Block a user