where('collection', 'voice_note') ->where('mime_type', '!=', 'audio/mp4') ->orderBy('id') ->get(); if ($pending->isEmpty()) { $this->info('Every voice note is already audio/mp4.'); return self::SUCCESS; } $this->info("{$pending->count()} voice note(s) to convert."); $converted = 0; $failed = 0; foreach ($pending as $media) { $disk = Storage::disk($media->disk); if (! $disk->exists($media->path)) { $this->warn("#{$media->id}: file missing ({$media->path}), skipped."); $failed++; continue; } if ($this->option('dry-run')) { $this->line("#{$media->id}: {$media->mime_type} → audio/mp4"); continue; } $oldPath = $media->path; $source = $disk->path($oldPath); $targetPath = preg_replace('/\.[A-Za-z0-9]+$/', '', $oldPath).'.m4a'; $target = $disk->path($targetPath); if (! $probe->transcodeAudioToM4a($source, $target)) { $this->error("#{$media->id}: ffmpeg could not convert {$media->path}."); $failed++; continue; } $media->update([ 'path' => $targetPath, 'mime_type' => 'audio/mp4', 'size' => (int) filesize($target), 'duration_ms' => $probe->durationMs($target) ?? $media->duration_ms, ]); if ($targetPath !== $oldPath) { $disk->delete($oldPath); } $this->line("#{$media->id}: converted."); $converted++; } if ($this->option('dry-run')) { return self::SUCCESS; } $this->info("Converted {$converted}, failed {$failed}."); return $failed === 0 ? self::SUCCESS : self::FAILURE; } }