'', 'fhdi' => '', 'uhd' => '', 'uhdi' => '']; public bool $censored = false; /** @var array */ public array $validation = []; public bool $overrideValidation = false; public bool $saving = false; public function mount(int $episodeId): void { abort_unless(auth()->user()?->hasRole(UserRole::ADMINISTRATOR), 403); $reference = Episode::with(['hentai', 'studio'])->findOrFail($episodeId); $this->referenceEpisodeId = $reference->id; $this->episodeNumber = $reference->hentai->episodes()->count() + 1; $this->title = $reference->title; $this->titleJpn = $reference->title_jpn; $this->studio = $reference->studio?->name ?? ''; $this->tags = $reference->tags->pluck('name')->all(); $this->releasedate = $reference->release_date ? Carbon::parse($reference->release_date)->format('Y-m-d') : now()->format('Y-m-d'); $this->baseurl = preg_replace('#/E\d+$#', '', $reference->url) ?? ''; $this->description = $reference->description; } public function getHasInvalidValidationProperty(): bool { foreach ($this->validation as $status) { if (($status['state'] ?? '') === 'invalid') { return true; } } return false; } public function updatedBaseurl(): void { $this->validateStream(); } public function updatedDownloads(mixed $value, string $key): void { if (preg_match('/^(fhd|fhdi|uhd|uhdi)$/', $key)) { $this->validateDownload($key); } } 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, 1, false, $this->episodeNumber); $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 validateDownload(string $quality): void { $key = "downloads.{$quality}"; $url = $this->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 save(): void { abort_unless(auth()->user()?->hasRole(UserRole::ADMINISTRATOR), 403); $this->validate([ 'baseurl' => ['required', 'string', 'regex:/^[A-Za-z0-9_.\-]+\/[A-Za-z0-9_.\-]+/'], 'description' => 'required|string', 'cover' => 'required|image|max:20480', 'gallery.*' => 'nullable|image|max:20480', 'downloads.fhd' => 'required|string', 'downloads.uhd' => 'required|string', 'downloads.fhdi' => 'nullable|string', 'downloads.uhdi' => 'nullable|string', ]); $this->saving = true; try { // Re-load the reference episode fresh so the episode number reflects // the current episode count rather than the mount-time value. $referenceEpisode = Episode::with(['hentai', 'studio'])->findOrFail($this->referenceEpisodeId); $hentai = $referenceEpisode->hentai; $this->episodeNumber = $hentai->episodes()->count() + 1; // Re-validate every non-empty download path + stream against the CDN. $validator = app(CdnPathValidator::class); $sizes = []; foreach ($this->downloads as $quality => $url) { if (trim($url) === '') { continue; } $type = $this->qualityToType($quality); $key = "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[$type] = $result['size']; if (! $result['valid'] && ! $this->overrideValidation) { $this->addError($key, 'Path not found on all CDN mirrors.'); $this->saving = false; return; } } $result = $validator->validateStream($this->baseurl, 1, true, $this->episodeNumber); $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; } // Persist. $episodeService = app(EpisodeService::class); $studio = $episodeService->getOrCreateStudio(trim($this->studio)); $episodeModel = $episodeService->createEpisodeFromArray([ 'title' => $this->title, 'title_jpn' => $this->titleJpn, 'baseurl' => $this->baseurl, 'description' => $this->description, 'releasedate' => $this->releasedate, 'tags' => $this->tags, 'interpolated_uhd' => trim($this->downloads['uhdi'] ?? '') !== '', ], $hentai, $this->episodeNumber, $studio); if ($this->cover) { $episodeService->saveCoverFromFile($episodeModel, $hentai->slug, $this->episodeNumber, $this->cover); } if (! empty($this->gallery)) { app(GalleryService::class)->saveGalleryFiles($hentai, $episodeModel, $this->episodeNumber, $this->gallery); } $downloads = []; foreach ($this->downloads as $quality => $url) { if (trim($url) === '') { continue; } $type = $this->qualityToType($quality); $downloads[$type] = [ 'url' => trim($url), 'size' => $sizes[$type] ?? null, ]; } app(DownloadService::class)->createOrUpdateDownloadsFromArray($episodeModel, $downloads); // Discord notifications + cache flush. if ($this->censored) { DiscordReleaseNotification::dispatch($this->title.' - '.$this->episodeNumber, 'release-censored'); } else { DiscordReleaseNotification::dispatch($episodeModel->slug, 'release'); } cache()->flush(); } catch (\Throwable $e) { Log::error('Failed to create episode from Livewire form: '.$e->getMessage(), [ 'exception' => $e, 'referenceEpisodeId' => $this->referenceEpisodeId, ]); $this->saving = false; $this->addError('description', 'Something went wrong while saving. Please try again.'); return; } $this->redirectRoute('hentai.index', ['title' => $episodeModel->slug]); } private function qualityToType(string $quality): string { return match ($quality) { 'fhd' => 'FHD', 'fhdi' => 'FHDi', 'uhd' => 'UHD', 'uhdi' => 'UHDi', }; } public function render() { return view('livewire.admin-episode-form'); } }