fix(pinterest,youtube): default video cover and gate empty YouTube posts
Pinterest video pins failed with "provide cover_image_url ... or cover_image_key_frame_time" because no cover was sent. Default to cover_image_key_frame_time 0 (first frame) when the user provides no cover_image_url — verified against Pinterest's OpenAPI spec (integer seconds, minimum 0, falls back to the last frame past the duration). YouTube Shorts derive their required title from the post content, so the publisher rejects an empty post — but the editor still let it be scheduled. Gate it in compliance: a youtube_short with empty content is now flagged (requires_text) so it can't be scheduled, instead of failing at publish.
This commit is contained in:
parent
f0e2907ab5
commit
27151bd925
6 changed files with 67 additions and 3 deletions
|
|
@ -273,6 +273,8 @@ private function publishVideoPin(PostPlatform $postPlatform, ?string $content):
|
|||
|
||||
if (! empty(data_get($postPlatform->meta, 'cover_image_url'))) {
|
||||
$payload['media_source']['cover_image_url'] = data_get($postPlatform->meta, 'cover_image_url');
|
||||
} else {
|
||||
$payload['media_source']['cover_image_key_frame_time'] = 0;
|
||||
}
|
||||
|
||||
$response = $this->socialHttp()->withToken($account->access_token)
|
||||
|
|
|
|||
|
|
@ -353,6 +353,7 @@
|
|||
'document_too_large' => 'PDF exceeds the size limit for this platform.',
|
||||
'aspect_ratio_invalid' => 'Aspect ratio is not supported by this format.',
|
||||
'no_content_type' => 'Pick a content type for this platform.',
|
||||
'requires_text' => 'Add text — this format needs a title.',
|
||||
],
|
||||
'publishing' => 'Publishing...',
|
||||
'publishing_overlay_title' => 'Your post is being published',
|
||||
|
|
|
|||
|
|
@ -353,6 +353,7 @@
|
|||
'document_too_large' => 'El PDF supera el límite de tamaño de esta plataforma.',
|
||||
'aspect_ratio_invalid' => 'La proporción de aspecto no es compatible con este formato.',
|
||||
'no_content_type' => 'Elige un tipo de contenido para esta plataforma.',
|
||||
'requires_text' => 'Agrega texto — este formato necesita un título.',
|
||||
],
|
||||
'publishing' => 'Publicando...',
|
||||
'publishing_overlay_title' => 'Tu publicación se está enviando',
|
||||
|
|
|
|||
|
|
@ -353,6 +353,7 @@
|
|||
'document_too_large' => 'O PDF excede o limite de tamanho desta plataforma.',
|
||||
'aspect_ratio_invalid' => 'A proporção da imagem não é suportada por este formato.',
|
||||
'no_content_type' => 'Escolha um tipo de conteúdo para esta plataforma.',
|
||||
'requires_text' => 'Adicione texto — este formato precisa de um título.',
|
||||
],
|
||||
'publishing' => 'Publicando...',
|
||||
'publishing_overlay_title' => 'Seu post está sendo publicado',
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ export const PLATFORM_VARIANTS: Record<string, string[]> = {
|
|||
[Platform.Pinterest]: [ContentType.PinterestPin, ContentType.PinterestVideoPin, ContentType.PinterestCarousel],
|
||||
};
|
||||
|
||||
// Content types whose post needs text to publish — YouTube Shorts derive their
|
||||
// required title from the post content, so an empty post can't be scheduled.
|
||||
const CONTENT_TYPES_REQUIRING_TEXT = new Set<string>([ContentType.YouTubeShort]);
|
||||
|
||||
type MetaRule = (meta: Record<string, any>) => { valid: boolean; tooltipKey: string | null };
|
||||
|
||||
// Platforms whose `meta` blob has publish-time requirements. `valid` gates
|
||||
|
|
@ -198,6 +202,11 @@ export const usePostCompliance = (opts: UsePostComplianceOptions) => {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (CONTENT_TYPES_REQUIRING_TEXT.has(contentType) && content.value.trim() === '') {
|
||||
issues[pp.id] = trans('posts.edit.compliance.requires_text');
|
||||
continue;
|
||||
}
|
||||
|
||||
const reason = getMediaIncompatibilityReason(contentType, media.value);
|
||||
if (!reason) continue;
|
||||
|
||||
|
|
|
|||
|
|
@ -481,10 +481,60 @@
|
|||
expect($result['id'])->toBe('video_pin_999');
|
||||
expect($result['url'])->toBe('https://pinterest.com/pin/video_pin_999');
|
||||
|
||||
Http::assertSent(function ($request) {
|
||||
return str_contains($request->url(), '/v5/pins')
|
||||
&& data_get($request->data(), 'media_source.source_type') === 'video_id';
|
||||
Http::assertSent(fn ($request) => $request->url() === config('trypost.platforms.pinterest.api').'/pins'
|
||||
&& data_get($request->data(), 'media_source.source_type') === 'video_id'
|
||||
&& data_get($request->data(), 'media_source.cover_image_key_frame_time') === 0);
|
||||
});
|
||||
|
||||
test('pinterest video pin uses the provided cover image url over the default frame', function () {
|
||||
$this->postPlatform->update([
|
||||
'content_type' => ContentType::PinterestVideoPin,
|
||||
'meta' => ['board_id' => 'board_123', 'cover_image_url' => 'https://example.com/cover.jpg'],
|
||||
]);
|
||||
|
||||
$this->post->update([
|
||||
'media' => [[
|
||||
'id' => 'test-media-video',
|
||||
'path' => 'media/2026-01/video.mp4',
|
||||
'url' => 'https://example.com/media/2026-01/video.mp4',
|
||||
'mime_type' => 'video/mp4',
|
||||
'original_filename' => 'video.mp4',
|
||||
]],
|
||||
]);
|
||||
|
||||
$s3UploadUrl = 'https://pinterest-media-upload.s3.amazonaws.com/upload';
|
||||
|
||||
Http::fake(function ($request) use ($s3UploadUrl) {
|
||||
$url = $request->url();
|
||||
|
||||
if (str_contains($url, '/v5/media') && $request->method() === 'POST') {
|
||||
return Http::response([
|
||||
'media_id' => 'media_video_789',
|
||||
'upload_url' => $s3UploadUrl,
|
||||
'upload_parameters' => [],
|
||||
], 201);
|
||||
}
|
||||
|
||||
if ($url === $s3UploadUrl) {
|
||||
return Http::response('', 204);
|
||||
}
|
||||
|
||||
if (str_contains($url, '/v5/media/media_video_789')) {
|
||||
return Http::response(['status' => 'succeeded'], 200);
|
||||
}
|
||||
|
||||
if (str_contains($url, '/v5/pins')) {
|
||||
return Http::response(['id' => 'video_pin_cover'], 200);
|
||||
}
|
||||
|
||||
return Http::response('fake-video-content', 200);
|
||||
});
|
||||
|
||||
$this->publisher->publish($this->postPlatform);
|
||||
|
||||
Http::assertSent(fn ($request) => $request->url() === config('trypost.platforms.pinterest.api').'/pins'
|
||||
&& data_get($request->data(), 'media_source.cover_image_url') === 'https://example.com/cover.jpg'
|
||||
&& data_get($request->data(), 'media_source.cover_image_key_frame_time') === null);
|
||||
});
|
||||
|
||||
test('pinterest publisher throws exception for unsupported content type', function () {
|
||||
|
|
|
|||
Loading…
Reference in a new issue