Files
AndyTranscribe/app/Actions/StoreUploadedRecordings.php
ben 34ccf0c32b Move recordings UI to Livewire pages with Flux components.
Replace controller-driven Blade views with full-page Livewire index/show/create flows so Flux tables, toasts, and uploads work natively.
2026-08-12 19:17:52 +02:00

131 lines
3.9 KiB
PHP

<?php
namespace App\Actions;
use App\Models\Recording;
use App\Models\User;
use App\Services\Mp3MetadataService;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
class StoreUploadedRecordings
{
public function __construct(private Mp3MetadataService $metadata) {}
/**
* Persist uploaded audio files and queue transcription.
*
* @param list<UploadedFile> $files
* @return array{
* recordings: list<Recording>,
* skipped_duplicates: int,
* message: string,
* }
*/
public function handle(User $user, array $files, ?string $titleOverride = null): array
{
$recordings = [];
$skippedDuplicates = 0;
$seenHashes = [];
foreach ($files as $file) {
if (! $file instanceof UploadedFile) {
continue;
}
$hash = hash_file('sha256', $file->getRealPath());
if ($hash === false) {
continue;
}
if (
isset($seenHashes[$hash])
|| $user->recordings()->where('content_hash', $hash)->exists()
|| $user->recordings()
->where('original_filename', $file->getClientOriginalName())
->where('file_size_bytes', $file->getSize() ?: 0)
->exists()
) {
$skippedDuplicates++;
continue;
}
$seenHashes[$hash] = true;
$title = count($files) === 1 && filled($titleOverride)
? $titleOverride
: null;
$recordings[] = $this->storeUploadedRecording($user, $file, $hash, $title);
}
$message = $this->message(count($recordings), $skippedDuplicates);
return [
'recordings' => $recordings,
'skipped_duplicates' => $skippedDuplicates,
'message' => $message,
];
}
private function storeUploadedRecording(
User $user,
UploadedFile $file,
string $contentHash,
?string $titleOverride = null,
): Recording {
$path = $file->store('recordings', 'local');
$absolutePath = Storage::disk('local')->path($path);
$tags = $this->metadata->extract($absolutePath);
$title = $titleOverride
?: ($tags['title'] ?? pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME));
$recording = Recording::query()->create([
'user_id' => $user->id,
'title' => $title,
'original_filename' => $file->getClientOriginalName(),
'file_path' => $path,
'duration_seconds' => $tags['duration_seconds'],
'recorded_at' => $tags['recorded_at'],
'artist' => $tags['artist'],
'album' => $tags['album'],
'file_size_bytes' => $file->getSize() ?: 0,
'content_hash' => $contentHash,
'transcription_status' => 'pending',
'transcription_driver' => 'local',
]);
$recording->queueLocalTranscription();
return $recording->fresh();
}
private function message(int $savedCount, int $skippedDuplicates): string
{
if ($savedCount === 0 && $skippedDuplicates > 0) {
return $skippedDuplicates === 1
? 'That file is already uploaded — nothing new was saved.'
: "All {$skippedDuplicates} files were duplicates — nothing new was saved.";
}
if ($savedCount === 0) {
return 'No valid audio files were uploaded.';
}
$message = $savedCount === 1
? 'Recording uploaded — transcription queued.'
: $savedCount.' recordings uploaded — transcription queued.';
if ($skippedDuplicates > 0) {
$message .= $skippedDuplicates === 1
? ' Skipped 1 duplicate.'
: " Skipped {$skippedDuplicates} duplicates.";
}
return $message;
}
}