- Event-Medien tragen Typ (image/video/audio), Dauer und Notiz; Videos bekommen ein Poster, Audio wird per ffmpeg nach AAC (audio/mp4) gewandelt, Originale werden mit Range-Unterstützung gestreamt. - Upload nimmt eine Notiz (caption) direkt mit, PATCH ändert sie später. - Neue Event-Felder: Zeitraum (end_date), Kategorie, Personen, Meilenstein. Regeln zentral in EventFields, damit CRUD und Sync gleich prüfen. - Titel darf leer sein: ein Event entsteht mit dem ersten Eintrag. - Artisan-Befehl media:transcode-voice-notes wandelt alte WebM/Ogg-Notizen, die Safari nicht abspielen kann. - Login-Throttling, Settings-Validierung, Sync-Fehlercodes ohne Rohtext. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
123 lines
4 KiB
PHP
123 lines
4 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Http\Requests\StoreEventMediaRequest;
|
|
use App\Http\Requests\UpdateEventMediaRequest;
|
|
use App\Http\Resources\EventMediaResource;
|
|
use App\Models\EventMedia;
|
|
use App\Services\EventMediaStore;
|
|
use App\Services\MediaCleaner;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Response;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
|
|
|
class EventMediaController extends Controller
|
|
{
|
|
public function __construct(private readonly MediaCleaner $mediaCleaner) {}
|
|
|
|
public function index(Request $request, string $clientId): JsonResponse
|
|
{
|
|
$event = $request->user()->events()
|
|
->where('client_id', $clientId)
|
|
->firstOrFail();
|
|
|
|
return response()->json([
|
|
'data' => EventMediaResource::collection(
|
|
$event->media()->orderBy('created_at')->get()
|
|
),
|
|
]);
|
|
}
|
|
|
|
public function store(
|
|
StoreEventMediaRequest $request,
|
|
EventMediaStore $store,
|
|
string $clientId
|
|
): JsonResponse {
|
|
$event = $request->user()->events()
|
|
->where('client_id', $clientId)
|
|
->firstOrFail();
|
|
|
|
$media = $store->store(
|
|
$event,
|
|
$request->file('file'),
|
|
$request->validated('collection') ?? 'gallery',
|
|
$request->validated('caption'),
|
|
);
|
|
|
|
return (new EventMediaResource($media))
|
|
->response()
|
|
->setStatusCode(201);
|
|
}
|
|
|
|
public function show(Request $request, EventMedia $media, string $variant): Response|BinaryFileResponse
|
|
{
|
|
abort_unless(in_array($variant, ['thumb', 'preview', 'original'], true), 404);
|
|
abort_unless((int) $media->user_id === (int) $request->user()->id, 404);
|
|
|
|
$path = match ($variant) {
|
|
'thumb' => $media->thumbnail_path,
|
|
'preview' => $media->preview_path ?: $media->thumbnail_path,
|
|
default => $media->path,
|
|
};
|
|
|
|
abort_unless(is_string($path) && $path !== '', 404);
|
|
abort_unless(Storage::disk($media->disk)->exists($path), 404);
|
|
|
|
$headers = ['Cache-Control' => 'private, max-age=604800'];
|
|
|
|
if ($variant !== 'original') {
|
|
// Thumbnails and previews are always JPEG, whatever the original.
|
|
return response(Storage::disk($media->disk)->get($path), 200, [
|
|
...$headers,
|
|
'Content-Type' => 'image/jpeg',
|
|
]);
|
|
}
|
|
|
|
// Originals may be videos or audio: a file response streams them and
|
|
// honours Range requests, which media players need for seeking.
|
|
return response()->file(Storage::disk($media->disk)->path($path), [
|
|
...$headers,
|
|
'Content-Type' => $media->mime_type ?: 'application/octet-stream',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* PATCH /api/events/{clientId}/media/{media} — the caption only; the
|
|
* file itself is immutable.
|
|
*/
|
|
public function update(UpdateEventMediaRequest $request, string $clientId, EventMedia $media): EventMediaResource
|
|
{
|
|
$event = $request->user()->events()
|
|
->where('client_id', $clientId)
|
|
->firstOrFail();
|
|
|
|
abort_unless((int) $media->event_id === (int) $event->id, 404);
|
|
|
|
$media->update(['caption' => $request->validated('caption') ?? '']);
|
|
|
|
return new EventMediaResource($media->fresh());
|
|
}
|
|
|
|
public function destroy(Request $request, string $clientId, EventMedia $media): JsonResponse
|
|
{
|
|
$event = $request->user()->events()
|
|
->where('client_id', $clientId)
|
|
->firstOrFail();
|
|
|
|
abort_unless((int) $media->event_id === (int) $event->id, 404);
|
|
|
|
$wasKeyImage = $media->collection === 'key_image';
|
|
$this->mediaCleaner->purgeMedia($media);
|
|
$media->delete();
|
|
|
|
if ($wasKeyImage) {
|
|
$event->update(['image' => null]);
|
|
}
|
|
|
|
return response()->json(null, 204);
|
|
}
|
|
}
|