feat: integrate MediaOptimizer into all image-uploading publishers
This commit is contained in:
parent
02298f31a6
commit
4ddb4d941a
7 changed files with 143 additions and 40 deletions
|
|
@ -4,10 +4,12 @@
|
|||
|
||||
namespace App\Services\Social;
|
||||
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Exceptions\Social\BlueskyPublishException;
|
||||
use App\Exceptions\TokenExpiredException;
|
||||
use App\Models\PostPlatform;
|
||||
use App\Models\SocialAccount;
|
||||
use App\Services\Media\MediaOptimizer;
|
||||
use Illuminate\Http\Client\Response;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
|
@ -124,12 +126,13 @@ private function uploadBlob(SocialAccount $account, string $service, string $url
|
|||
return null;
|
||||
}
|
||||
|
||||
// Bluesky has 1MB limit for images
|
||||
if (str_starts_with($mimeType, 'image/') && $fileSize > 1000000) {
|
||||
Log::warning('Bluesky image exceeds 1MB limit', [
|
||||
'size' => $fileSize,
|
||||
'url' => $url,
|
||||
]);
|
||||
// Optimize images for Bluesky's 1MB limit
|
||||
if (str_starts_with($mimeType, 'image/') && ! str_starts_with($mimeType, 'image/gif')) {
|
||||
$optimizer = app(MediaOptimizer::class);
|
||||
$optimizedPath = $optimizer->optimizeImage($tempFile, Platform::Bluesky);
|
||||
@unlink($tempFile);
|
||||
$tempFile = $optimizedPath;
|
||||
$mimeType = 'image/jpeg';
|
||||
}
|
||||
|
||||
$stream = fopen($tempFile, 'r');
|
||||
|
|
|
|||
|
|
@ -5,10 +5,12 @@
|
|||
namespace App\Services\Social;
|
||||
|
||||
use App\Enums\PostPlatform\ContentType;
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Exceptions\Social\LinkedInPublishException;
|
||||
use App\Exceptions\TokenExpiredException;
|
||||
use App\Models\PostPlatform;
|
||||
use App\Models\SocialAccount;
|
||||
use App\Services\Media\MediaOptimizer;
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Illuminate\Http\Client\Response;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
|
@ -275,24 +277,40 @@ private function uploadImage($mediaItem, string $ownerUrn): ?string
|
|||
|
||||
Log::info('LinkedIn Page image init success', ['imageUrn' => $imageUrn]);
|
||||
|
||||
// Step 2: Upload binary data
|
||||
$imageContent = file_get_contents($mediaItem->url);
|
||||
// Step 2: Download and optimize image
|
||||
$tempFile = tempnam(sys_get_temp_dir(), 'lip_image_');
|
||||
|
||||
$uploadResponse = Http::withToken($this->accessToken)
|
||||
->withHeaders([
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
])
|
||||
->withBody($imageContent, 'application/octet-stream')
|
||||
->put($uploadUrl);
|
||||
try {
|
||||
Http::withOptions(['sink' => $tempFile])->timeout(600)->get($mediaItem->url);
|
||||
|
||||
if ($uploadResponse->failed()) {
|
||||
Log::error('LinkedIn Page image upload failed', ['body' => $uploadResponse->body()]);
|
||||
$this->handleApiError($uploadResponse, 'Failed to upload LinkedIn Page image');
|
||||
$detectedMime = mime_content_type($tempFile) ?: '';
|
||||
if (str_starts_with($detectedMime, 'image/') && ! str_starts_with($detectedMime, 'image/gif')) {
|
||||
$optimizer = app(MediaOptimizer::class);
|
||||
$optimizedPath = $optimizer->optimizeImage($tempFile, Platform::LinkedInPage);
|
||||
@unlink($tempFile);
|
||||
$tempFile = $optimizedPath;
|
||||
}
|
||||
|
||||
$imageContent = file_get_contents($tempFile);
|
||||
|
||||
$uploadResponse = Http::withToken($this->accessToken)
|
||||
->withHeaders([
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
])
|
||||
->withBody($imageContent, 'application/octet-stream')
|
||||
->put($uploadUrl);
|
||||
|
||||
if ($uploadResponse->failed()) {
|
||||
Log::error('LinkedIn Page image upload failed', ['body' => $uploadResponse->body()]);
|
||||
$this->handleApiError($uploadResponse, 'Failed to upload LinkedIn Page image');
|
||||
}
|
||||
|
||||
Log::info('LinkedIn Page image upload success', ['imageUrn' => $imageUrn]);
|
||||
|
||||
return $imageUrn;
|
||||
} finally {
|
||||
@unlink($tempFile);
|
||||
}
|
||||
|
||||
Log::info('LinkedIn Page image upload success', ['imageUrn' => $imageUrn]);
|
||||
|
||||
return $imageUrn;
|
||||
}
|
||||
|
||||
private function uploadVideo($mediaItem, string $ownerUrn): ?string
|
||||
|
|
|
|||
|
|
@ -5,10 +5,12 @@
|
|||
namespace App\Services\Social;
|
||||
|
||||
use App\Enums\PostPlatform\ContentType;
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Exceptions\Social\LinkedInPublishException;
|
||||
use App\Exceptions\TokenExpiredException;
|
||||
use App\Models\PostPlatform;
|
||||
use App\Models\SocialAccount;
|
||||
use App\Services\Media\MediaOptimizer;
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Illuminate\Http\Client\Response;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
|
@ -256,24 +258,40 @@ private function uploadImage($mediaItem, string $ownerUrn): ?string
|
|||
|
||||
Log::info('LinkedIn image init success', ['imageUrn' => $imageUrn]);
|
||||
|
||||
// Step 2: Upload binary data
|
||||
$imageContent = file_get_contents($mediaItem->url);
|
||||
// Step 2: Download and optimize image
|
||||
$tempFile = tempnam(sys_get_temp_dir(), 'li_image_');
|
||||
|
||||
$uploadResponse = Http::withToken($this->accessToken)
|
||||
->withHeaders([
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
])
|
||||
->withBody($imageContent, 'application/octet-stream')
|
||||
->put($uploadUrl);
|
||||
try {
|
||||
Http::withOptions(['sink' => $tempFile])->timeout(600)->get($mediaItem->url);
|
||||
|
||||
if ($uploadResponse->failed()) {
|
||||
Log::error('LinkedIn image upload failed', ['body' => $uploadResponse->body()]);
|
||||
$this->handleApiError($uploadResponse, 'Failed to upload LinkedIn image');
|
||||
$detectedMime = mime_content_type($tempFile) ?: '';
|
||||
if (str_starts_with($detectedMime, 'image/') && ! str_starts_with($detectedMime, 'image/gif')) {
|
||||
$optimizer = app(MediaOptimizer::class);
|
||||
$optimizedPath = $optimizer->optimizeImage($tempFile, Platform::LinkedIn);
|
||||
@unlink($tempFile);
|
||||
$tempFile = $optimizedPath;
|
||||
}
|
||||
|
||||
$imageContent = file_get_contents($tempFile);
|
||||
|
||||
$uploadResponse = Http::withToken($this->accessToken)
|
||||
->withHeaders([
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
])
|
||||
->withBody($imageContent, 'application/octet-stream')
|
||||
->put($uploadUrl);
|
||||
|
||||
if ($uploadResponse->failed()) {
|
||||
Log::error('LinkedIn image upload failed', ['body' => $uploadResponse->body()]);
|
||||
$this->handleApiError($uploadResponse, 'Failed to upload LinkedIn image');
|
||||
}
|
||||
|
||||
Log::info('LinkedIn image upload success', ['imageUrn' => $imageUrn]);
|
||||
|
||||
return $imageUrn;
|
||||
} finally {
|
||||
@unlink($tempFile);
|
||||
}
|
||||
|
||||
Log::info('LinkedIn image upload success', ['imageUrn' => $imageUrn]);
|
||||
|
||||
return $imageUrn;
|
||||
}
|
||||
|
||||
private function uploadVideo($mediaItem, string $ownerUrn): ?string
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@
|
|||
|
||||
namespace App\Services\Social;
|
||||
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Exceptions\Social\MastodonPublishException;
|
||||
use App\Models\PostPlatform;
|
||||
use App\Models\SocialAccount;
|
||||
use App\Services\Media\MediaOptimizer;
|
||||
use Illuminate\Http\Client\Response;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
|
@ -82,6 +84,15 @@ private function uploadMedia(SocialAccount $account, string $instance, string $u
|
|||
return null;
|
||||
}
|
||||
|
||||
// Optimize images (skip GIFs)
|
||||
$detectedMime = mime_content_type($tempFile) ?: '';
|
||||
if (str_starts_with($detectedMime, 'image/') && ! str_starts_with($detectedMime, 'image/gif')) {
|
||||
$optimizer = app(MediaOptimizer::class);
|
||||
$optimizedPath = $optimizer->optimizeImage($tempFile, Platform::Mastodon);
|
||||
@unlink($tempFile);
|
||||
$tempFile = $optimizedPath;
|
||||
}
|
||||
|
||||
$name = $filename ?? basename(parse_url($url, PHP_URL_PATH));
|
||||
if (empty($name)) {
|
||||
$name = 'media';
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@
|
|||
namespace App\Services\Social;
|
||||
|
||||
use App\Enums\PostPlatform\ContentType;
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Exceptions\Social\PinterestPublishException;
|
||||
use App\Models\PostPlatform;
|
||||
use App\Models\SocialAccount;
|
||||
use App\Services\Media\MediaOptimizer;
|
||||
use Illuminate\Http\Client\Response;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
|
@ -54,11 +56,31 @@ private function publishImagePin(PostPlatform $postPlatform): array
|
|||
'image_url' => $media->url,
|
||||
]);
|
||||
|
||||
// Download and optimize image
|
||||
$tempFile = tempnam(sys_get_temp_dir(), 'pin_image_');
|
||||
|
||||
try {
|
||||
Http::withOptions(['sink' => $tempFile])->timeout(600)->get($media->url);
|
||||
|
||||
$detectedMime = mime_content_type($tempFile) ?: '';
|
||||
if (str_starts_with($detectedMime, 'image/') && ! str_starts_with($detectedMime, 'image/gif')) {
|
||||
$optimizer = app(MediaOptimizer::class);
|
||||
$optimizedPath = $optimizer->optimizeImage($tempFile, Platform::Pinterest);
|
||||
@unlink($tempFile);
|
||||
$tempFile = $optimizedPath;
|
||||
}
|
||||
|
||||
$imageBase64 = base64_encode(file_get_contents($tempFile));
|
||||
} finally {
|
||||
@unlink($tempFile);
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'board_id' => $boardId,
|
||||
'media_source' => [
|
||||
'source_type' => 'image_url',
|
||||
'url' => $media->url,
|
||||
'source_type' => 'image_base64',
|
||||
'content_type' => 'image/jpeg',
|
||||
'data' => $imageBase64,
|
||||
],
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@
|
|||
|
||||
namespace App\Services\Social;
|
||||
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Exceptions\Social\XPublishException;
|
||||
use App\Exceptions\TokenExpiredException;
|
||||
use App\Models\PostPlatform;
|
||||
use App\Models\SocialAccount;
|
||||
use App\Services\Media\MediaOptimizer;
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Illuminate\Http\Client\Response;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
|
@ -111,6 +113,15 @@ private function uploadMedia($mediaItem): ?array
|
|||
try {
|
||||
Http::withOptions(['sink' => $tempFile])->timeout(600)->get($mediaItem->url);
|
||||
|
||||
// Optimize images (skip GIFs — they need special handling)
|
||||
if (str_starts_with($mimeType, 'image/') && ! str_starts_with($mimeType, 'image/gif')) {
|
||||
$optimizer = app(MediaOptimizer::class);
|
||||
$optimizedPath = $optimizer->optimizeImage($tempFile, Platform::X);
|
||||
@unlink($tempFile);
|
||||
$tempFile = $optimizedPath;
|
||||
$mimeType = 'image/jpeg';
|
||||
}
|
||||
|
||||
$fileSize = filesize($tempFile);
|
||||
$mediaCategory = $this->getMediaCategory($mimeType, $fileSize);
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
use App\Models\SocialAccount;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use App\Services\Media\MediaOptimizer;
|
||||
use App\Services\Social\PinterestPublisher;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
|
|
@ -42,6 +43,17 @@
|
|||
]);
|
||||
|
||||
$this->publisher = new PinterestPublisher;
|
||||
|
||||
// Mock MediaOptimizer to return the same temp file path
|
||||
$mockOptimizer = Mockery::mock(MediaOptimizer::class);
|
||||
$mockOptimizer->shouldReceive('optimizeImage')->andReturnUsing(function (string $tempFile) {
|
||||
// Copy to a new temp file to simulate optimization
|
||||
$optimized = tempnam(sys_get_temp_dir(), 'pin_opt_');
|
||||
copy($tempFile, $optimized);
|
||||
|
||||
return $optimized;
|
||||
});
|
||||
app()->instance(MediaOptimizer::class, $mockOptimizer);
|
||||
});
|
||||
|
||||
test('pinterest publisher can publish image pin', function () {
|
||||
|
|
@ -59,6 +71,7 @@
|
|||
'*/v5/pins' => Http::response([
|
||||
'id' => 'pin_123456',
|
||||
], 200),
|
||||
'*' => Http::response('fake-image-content', 200),
|
||||
]);
|
||||
|
||||
$result = $this->publisher->publish($this->postPlatform);
|
||||
|
|
@ -113,12 +126,14 @@
|
|||
'*/v5/pins' => Http::response([
|
||||
'id' => 'pin_123456',
|
||||
], 200),
|
||||
'*' => Http::response('fake-image-content', 200),
|
||||
]);
|
||||
|
||||
$this->publisher->publish($this->postPlatform);
|
||||
|
||||
Http::assertSent(function ($request) {
|
||||
return $request['board_id'] === 'board_123'; // from account meta
|
||||
return str_contains($request->url(), '/v5/pins')
|
||||
&& $request['board_id'] === 'board_123'; // from account meta
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -207,6 +222,7 @@
|
|||
'code' => 400,
|
||||
'message' => 'Invalid request',
|
||||
], 400),
|
||||
'*' => Http::response('fake-image-content', 200),
|
||||
]);
|
||||
|
||||
expect(fn () => $this->publisher->publish($this->postPlatform))
|
||||
|
|
@ -229,6 +245,7 @@
|
|||
'code' => 1,
|
||||
'message' => 'Invalid access token',
|
||||
], 401),
|
||||
'*' => Http::response('fake-image-content', 200),
|
||||
]);
|
||||
|
||||
expect(fn () => $this->publisher->publish($this->postPlatform))
|
||||
|
|
@ -257,6 +274,7 @@
|
|||
'*/v5/pins' => Http::response([
|
||||
'id' => 'pin_123456',
|
||||
], 200),
|
||||
'*' => Http::response('fake-image-content', 200),
|
||||
]);
|
||||
|
||||
$this->publisher->publish($this->postPlatform);
|
||||
|
|
@ -292,12 +310,14 @@
|
|||
'*/v5/pins' => Http::response([
|
||||
'id' => 'pin_123456',
|
||||
], 200),
|
||||
'*' => Http::response('fake-image-content', 200),
|
||||
]);
|
||||
|
||||
$this->publisher->publish($this->postPlatform);
|
||||
|
||||
Http::assertSent(function ($request) {
|
||||
return $request['title'] === 'My Pin Title'
|
||||
return str_contains($request->url(), '/v5/pins')
|
||||
&& $request['title'] === 'My Pin Title'
|
||||
&& $request['link'] === 'https://example.com/my-page';
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue