d08e280ce0
Replace the server-rendered episode upload form with a Livewire component, introducing an interactive admin experience that validates the new episode's stream path against all configured mirrors before saving. The controller-based `store` method and its route are removed, and episode creation now flows through the `AdminEpisodeForm` component with frontend upload handling and dynamic quality checks. Key changes: - Add `AdminEpisodeForm` Livewire component with file uploads, tag handling, and debounced stream/base-url validation. - Extend `CdnPathValidator::validateStream` to accept an episode number so the manifest path can point to the specific episode being added. - Remove the `store` method from `EpisodeController` and its POST route. - Delete the legacy `createEpisode` method from `EpisodeService`. - Replace the Blade upload modal with the Livewire view, including progress indicators and validation badges. - Add feature tests for the Livewire component and update unit tests for the CDN validator's episode-specific manifest check.
202 lines
6.9 KiB
PHP
202 lines
6.9 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Episode;
|
|
use App\Models\Hentai;
|
|
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
|
|
{
|
|
public function generateSlug(string $title): string
|
|
{
|
|
$slug = Str::slug($title);
|
|
$slugParts = explode('-', $slug);
|
|
$lastPart = $slugParts[array_key_last($slugParts)];
|
|
|
|
if (is_numeric($lastPart) && $lastPart < 1000) {
|
|
$slugParts[array_key_last($slugParts)] = 's'.$lastPart;
|
|
|
|
return implode('-', $slugParts);
|
|
}
|
|
|
|
return $slug;
|
|
}
|
|
|
|
private function applyTags(Request $request, Episode $episode): void
|
|
{
|
|
$tags = json_decode($request->input('tags'));
|
|
$newtags = [];
|
|
foreach ($tags as $t) {
|
|
$newtags[] = $t->value;
|
|
}
|
|
|
|
$newTagsTemp = $newtags;
|
|
$oldTagsTemp = $episode->tagNames();
|
|
|
|
sort($newTagsTemp);
|
|
sort($oldTagsTemp);
|
|
|
|
if ($newTagsTemp !== $oldTagsTemp) {
|
|
ModLog::create([
|
|
'moderator' => $request->user()->name,
|
|
'data' => sprintf(
|
|
'Updated Episode tags from %s to %s',
|
|
implode(', ', $oldTagsTemp),
|
|
implode(', ', $newTagsTemp),
|
|
),
|
|
]);
|
|
}
|
|
|
|
$episode->retag($newtags);
|
|
}
|
|
|
|
private function updateTitle(Request $request, Episode $episode): void
|
|
{
|
|
$updates = [];
|
|
|
|
if ($episode->title !== $request->input('title')) {
|
|
$updates['title'] = $request->input('title');
|
|
$updates['title_search'] = preg_replace(
|
|
'/[^A-Za-z0-9 ]/',
|
|
'',
|
|
$request->input('title')
|
|
);
|
|
|
|
// Log to ModLog
|
|
ModLog::create([
|
|
'moderator' => $request->user()->name,
|
|
'data' => "Updating Hentai Title from {$episode->title} to {$request->input('title')}",
|
|
]);
|
|
}
|
|
|
|
if ($episode->title_jpn !== $request->input('title_jpn')) {
|
|
$updates['title_jpn'] = $request->input('title_jpn');
|
|
|
|
// Log to ModLog
|
|
ModLog::create([
|
|
'moderator' => $request->user()->name,
|
|
'data' => "Updating Hentai Title from {$episode->title_jpn} to {$request->input('title_jpn')}",
|
|
]);
|
|
}
|
|
|
|
if (! empty($updates)) {
|
|
$episode->hentai->episodes()->update($updates);
|
|
}
|
|
}
|
|
|
|
public function updateEpisode(Request $request, Studios $studio, int $episodeId): Episode
|
|
{
|
|
$episode = Episode::where('id', $episodeId)->firstOrFail();
|
|
$episode->studios_id = $studio->id;
|
|
$episode->description = $request->input('description');
|
|
$episode->url = $request->input('baseurl');
|
|
$episode->release_date = Carbon::parse($request->input('releasedate'))->format('Y-m-d');
|
|
$episode->interpolated = $request->input('interpolated') == 'yes';
|
|
$episode->interpolated_uhd = $request->input('downloadUHDi1') ? true : false;
|
|
$episode->is_dvd_aspect = $request->input('dvd') == 'yes';
|
|
$episode->dmca_takedown = $request->input('dmca_takedown') == 'true';
|
|
$episode->save();
|
|
|
|
$this->applyTags($request, $episode);
|
|
$this->updateTitle($request, $episode);
|
|
|
|
return $episode;
|
|
}
|
|
|
|
public function updateEpisodeModerator(Request $request, int $episodeId): void
|
|
{
|
|
$episode = Episode::where('id', $episodeId)->firstOrFail();
|
|
$oldDescription = $episode->description;
|
|
$episode->description = $request->input('description');
|
|
$episode->save();
|
|
|
|
if ($episode->description !== $oldDescription) {
|
|
// Log to ModLog
|
|
ModLog::create([
|
|
'moderator' => $request->user()->name,
|
|
'data' => "Updated Episode description from {$oldDescription} to {$episode->description}",
|
|
]);
|
|
}
|
|
|
|
$this->applyTags($request, $episode);
|
|
$this->updateTitle($request, $episode);
|
|
}
|
|
|
|
public function getOrCreateStudio(string $studioName): Studios
|
|
{
|
|
return Studios::firstOrCreate(
|
|
['name' => $studioName],
|
|
['slug' => Str::slug($studioName)]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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($file->getRealPath())
|
|
->cover(268, 394)
|
|
->encode(new WebpEncoder)
|
|
->save(Storage::disk('public')->path($episode->cover_url));
|
|
}
|
|
}
|