trypost/app/DataTransferObjects/MediaItem.php
Paulo Castellano c5fca040a9 fix(tiktok): resize oversized photos before PULL_FROM_URL
TikTok photo posts hand the platform our media URLs and let it pull the
bytes, so they never went through MediaOptimizer like the upload-based
publishers do. An image wider than 1080px reached TikTok untouched and was
rejected with picture_size_check_failed ("Image dimensions exceed limits").

Photos within spec still pass through unchanged. Oversized ones (or images
with unknown dimensions) are downloaded, resized to TikTok's 1080px spec,
hosted on the public disk, and that URL is handed to TikTok instead. The
derivative is pruned once TikTok has pulled it.

Reuses the same hosted-derivative pattern Instagram/Threads already use for
aspect-ratio cropping; the TikTok max_width config was previously dead code.
2026-06-26 12:23:56 -03:00

101 lines
2.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\DataTransferObjects;
use App\Enums\Media\Source;
use App\Enums\Media\Type;
class MediaItem
{
/**
* @param array<string, mixed>|null $meta
* @param array<string, mixed>|null $source_meta
*/
public function __construct(
public readonly string $id,
public readonly string $path,
public readonly string $url,
public readonly ?string $mime_type = null,
public readonly ?string $original_filename = null,
public readonly ?Source $source = null,
public readonly ?array $source_meta = null,
public readonly ?array $meta = null,
) {}
public function isVideo(): bool
{
return Type::classify($this->mime_type, $this->path) === Type::Video;
}
public function isImage(): bool
{
return Type::classify($this->mime_type, $this->path) === Type::Image;
}
public function isDocument(): bool
{
return Type::classify($this->mime_type, $this->path) === Type::Document;
}
/**
* Stored pixel width from upload-time metadata, when known.
*/
public function width(): ?int
{
$width = data_get($this->meta, 'width');
return is_numeric($width) ? (int) $width : null;
}
/**
* Stored pixel height from upload-time metadata, when known.
*/
public function height(): ?int
{
$height = data_get($this->meta, 'height');
return is_numeric($height) ? (int) $height : null;
}
/**
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
$path = data_get($data, 'path', '');
$mimeType = data_get($data, 'mime_type');
if (! $mimeType && $path) {
$extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
$mimeType = match ($extension) {
'jpg', 'jpeg' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
'webp' => 'image/webp',
'mp4' => 'video/mp4',
'mov' => 'video/quicktime',
'pdf' => 'application/pdf',
default => null,
};
}
$sourceValue = data_get($data, 'source');
$source = is_string($sourceValue) ? Source::tryFrom($sourceValue) : null;
$sourceMeta = data_get($data, 'source_meta');
$meta = data_get($data, 'meta');
return new self(
id: data_get($data, 'id', ''),
path: $path,
url: data_get($data, 'url', ''),
mime_type: $mimeType,
original_filename: data_get($data, 'original_filename'),
source: $source,
source_meta: is_array($sourceMeta) ? $sourceMeta : null,
meta: is_array($meta) ? $meta : null,
);
}
}