Implement creator studio and upload updates

This commit is contained in:
2026-04-04 10:12:02 +02:00
parent 1da7d3bf88
commit 0b216b7ecd
15107 changed files with 31206 additions and 626514 deletions

View File

@@ -105,6 +105,13 @@ final class ArtworkDownloadController extends Controller
*/
private function resolveDownloadUrl(Artwork $artwork): string
{
$filePath = trim((string) ($artwork->file_path ?? ''), '/');
$cdn = rtrim((string) config('cdn.files_url', 'https://files.skinbase.org'), '/');
if ($filePath !== '') {
return $cdn . '/' . $filePath;
}
$hash = $artwork->hash ?? null;
$ext = ltrim((string) ($artwork->file_ext ?: $artwork->thumb_ext ?: 'webp'), '.');
@@ -112,9 +119,9 @@ final class ArtworkDownloadController extends Controller
$h = strtolower(preg_replace('/[^a-f0-9]/', '', $hash));
$h1 = substr($h, 0, 2);
$h2 = substr($h, 2, 2);
$cdn = rtrim((string) config('cdn.files_url', 'https://files.skinbase.org'), '/');
$prefix = trim((string) config('uploads.object_storage.prefix', 'artworks'), '/');
return sprintf('%s/original/%s/%s/%s.%s', $cdn, $h1, $h2, $h, $ext);
return sprintf('%s/%s/original/%s/%s/%s.%s', $cdn, $prefix, $h1, $h2, $h, $ext);
}
// Fallback: best available thumbnail size

View File

@@ -16,6 +16,7 @@ use App\Services\NovaCards\NovaCardDraftService;
use App\Services\NovaCards\NovaCardPresenter;
use App\Services\NovaCards\NovaCardPublishService;
use App\Services\NovaCards\NovaCardRenderService;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -115,9 +116,10 @@ class NovaCardDraftController extends Controller
public function publish(SaveNovaCardDraftRequest $request, int $id): JsonResponse
{
$card = $this->editableCard($request, $id);
$validated = $request->validated();
if ($request->validated() !== []) {
$card = $this->drafts->autosave($card, $request->validated());
if ($validated !== []) {
$card = $this->drafts->autosave($card, $validated);
}
if (trim((string) $card->title) === '' || trim((string) $card->quote_text) === '') {
@@ -126,6 +128,32 @@ class NovaCardDraftController extends Controller
], Response::HTTP_UNPROCESSABLE_ENTITY);
}
$publishMode = (string) ($validated['publish_mode'] ?? 'now');
if ($publishMode === 'schedule') {
if (empty($validated['scheduled_for'])) {
return response()->json([
'message' => 'Choose a date and time for scheduled publishing.',
], Response::HTTP_UNPROCESSABLE_ENTITY);
}
try {
$card = $this->publishes->schedule(
$card->loadMissing('backgroundImage'),
Carbon::parse((string) $validated['scheduled_for']),
isset($validated['scheduling_timezone']) ? (string) $validated['scheduling_timezone'] : null,
);
} catch (\InvalidArgumentException $exception) {
return response()->json([
'message' => $exception->getMessage(),
], Response::HTTP_UNPROCESSABLE_ENTITY);
}
return response()->json([
'data' => $this->presenter->card($card, true, $request->user()),
]);
}
$card = $this->publishes->publishNow($card->loadMissing('backgroundImage'));
event(new NovaCardPublished($card));

View File

@@ -83,6 +83,11 @@ final class UploadController extends Controller
$sessionId = (string) $request->validated('session_id');
$artworkId = (int) $request->validated('artwork_id');
$originalFileName = $request->validated('file_name');
$archiveSessionId = $request->validated('archive_session_id');
$archiveOriginalFileName = $request->validated('archive_file_name');
$additionalScreenshotSessions = collect($request->validated('additional_screenshot_sessions', []))
->filter(fn ($payload) => is_array($payload) && is_string($payload['session_id'] ?? null))
->values();
$session = $sessions->getOrFail($sessionId);
@@ -112,14 +117,81 @@ final class UploadController extends Controller
], Response::HTTP_UNPROCESSABLE_ENTITY);
}
$validatedArchive = null;
if (is_string($archiveSessionId) && trim($archiveSessionId) !== '') {
$validatedArchive = $pipeline->validateAndHashArchive($archiveSessionId);
if (! $validatedArchive->validation->ok || ! $validatedArchive->hash) {
return response()->json([
'message' => 'Archive validation failed.',
'reason' => $validatedArchive->validation->reason,
], Response::HTTP_UNPROCESSABLE_ENTITY);
}
$archiveScan = $pipeline->scan($archiveSessionId);
if (! $archiveScan->ok) {
return response()->json([
'message' => 'Archive scan failed.',
'reason' => $archiveScan->reason,
], Response::HTTP_UNPROCESSABLE_ENTITY);
}
}
$validatedAdditionalScreenshots = [];
foreach ($additionalScreenshotSessions as $payload) {
$screenshotSessionId = (string) ($payload['session_id'] ?? '');
if ($screenshotSessionId === '') {
continue;
}
$validatedScreenshot = $pipeline->validateAndHash($screenshotSessionId);
if (! $validatedScreenshot->validation->ok || ! $validatedScreenshot->hash) {
return response()->json([
'message' => 'Screenshot validation failed.',
'reason' => $validatedScreenshot->validation->reason,
], Response::HTTP_UNPROCESSABLE_ENTITY);
}
$screenshotScan = $pipeline->scan($screenshotSessionId);
if (! $screenshotScan->ok) {
return response()->json([
'message' => 'Screenshot scan failed.',
'reason' => $screenshotScan->reason,
], Response::HTTP_UNPROCESSABLE_ENTITY);
}
$validatedAdditionalScreenshots[] = [
'session_id' => $screenshotSessionId,
'hash' => $validatedScreenshot->hash,
'file_name' => is_string($payload['file_name'] ?? null) ? $payload['file_name'] : null,
];
}
try {
$status = DB::transaction(function () use ($pipeline, $sessionId, $validated, $artworkId, $originalFileName) {
$status = DB::transaction(function () use ($pipeline, $sessionId, $validated, $artworkId, $originalFileName, $archiveSessionId, $validatedArchive, $archiveOriginalFileName, $validatedAdditionalScreenshots) {
if ((bool) config('uploads.queue_derivatives', false)) {
GenerateDerivativesJob::dispatch($sessionId, $validated->hash, $artworkId, is_string($originalFileName) ? $originalFileName : null)->afterCommit();
GenerateDerivativesJob::dispatch(
$sessionId,
$validated->hash,
$artworkId,
is_string($originalFileName) ? $originalFileName : null,
is_string($archiveSessionId) ? $archiveSessionId : null,
$validatedArchive?->hash,
is_string($archiveOriginalFileName) ? $archiveOriginalFileName : null,
$validatedAdditionalScreenshots
)->afterCommit();
return 'queued';
}
$pipeline->processAndPublish($sessionId, $validated->hash, $artworkId, is_string($originalFileName) ? $originalFileName : null);
$pipeline->processAndPublish(
$sessionId,
$validated->hash,
$artworkId,
is_string($originalFileName) ? $originalFileName : null,
is_string($archiveSessionId) ? $archiveSessionId : null,
$validatedArchive?->hash,
is_string($archiveOriginalFileName) ? $archiveOriginalFileName : null,
$validatedAdditionalScreenshots
);
// Derivatives are available now; dispatch AI auto-tagging.
AutoTagArtworkJob::dispatch($artworkId, $validated->hash)->afterCommit();
@@ -132,6 +204,8 @@ final class UploadController extends Controller
'hash' => $validated->hash,
'artwork_id' => $artworkId,
'status' => $status,
'archive_session_id' => is_string($archiveSessionId) ? $archiveSessionId : null,
'additional_screenshot_session_ids' => array_values(array_map(static fn (array $payload): string => (string) $payload['session_id'], $validatedAdditionalScreenshots)),
]);
return response()->json([
@@ -540,13 +614,6 @@ final class UploadController extends Controller
$slugBase = 'artwork';
}
$slug = $slugBase;
$suffix = 2;
while (Artwork::query()->where('slug', $slug)->where('id', '!=', $artwork->id)->exists()) {
$slug = $slugBase . '-' . $suffix;
$suffix++;
}
$artwork->title = $title;
if (array_key_exists('description', $validated)) {
$artwork->description = $validated['description'];
@@ -554,7 +621,7 @@ final class UploadController extends Controller
if (array_key_exists('is_mature', $validated) || array_key_exists('nsfw', $validated)) {
$artwork->is_mature = (bool) ($validated['is_mature'] ?? $validated['nsfw'] ?? false);
}
$artwork->slug = $slug;
$artwork->slug = Str::limit($slugBase, 160, '');
$artwork->artwork_timezone = $validated['timezone'] ?? null;
// Sync category if provided