teknolis / app /Services /FreepikStockContentService.php
adwandw's picture
Enable HF-only mode: add hf_only config, always show Remix Lab, hide Remix Stock in HF-only, improve error message for stock remix
da7bbba
Raw
History Blame Contribute Delete
11.7 kB
<?php
namespace App\Services;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use RuntimeException;
class FreepikStockContentService
{
public function search(string $query, string $type = 'all', string $order = 'relevance', int $page = 1): array
{
$apiKey = trim((string) config('services.freepik.api_key', ''));
$cleanQuery = trim($query);
$type = $this->normalizeType($type);
$order = in_array($order, ['relevance', 'recent'], true) ? $order : 'relevance';
$page = max(1, $page);
if ($apiKey === '') {
throw new RuntimeException($this->missingApiKeyMessage());
}
if ($cleanQuery === '') {
return [
'results' => [
'images' => [],
'videos' => [],
'icons' => [],
'audio' => [],
],
'errors' => [],
'summary' => [
'images' => 0,
'videos' => 0,
'icons' => 0,
'audio' => 0,
],
];
}
$targets = $type === 'all'
? ['images', 'videos', 'icons', 'audio']
: [$type];
$results = [
'images' => [],
'videos' => [],
'icons' => [],
'audio' => [],
];
$errors = [];
foreach ($targets as $target) {
try {
$results[$target] = match ($target) {
'images' => $this->searchResources($apiKey, $cleanQuery, $order, $page),
'videos' => $this->searchVideos($apiKey, $cleanQuery, $order, $page),
'icons' => $this->searchIcons($apiKey, $cleanQuery, $order, $page),
'audio' => $this->searchMusic($apiKey, $cleanQuery, $page),
default => [],
};
} catch (\Throwable $error) {
$errors[$target] = $error->getMessage();
}
}
return [
'results' => $results,
'errors' => $errors,
'summary' => [
'images' => count($results['images']),
'videos' => count($results['videos']),
'icons' => count($results['icons']),
'audio' => count($results['audio']),
],
];
}
private function searchResources(string $apiKey, string $query, string $order, int $page): array
{
$response = $this->client($apiKey)
->get($this->endpoint('/v1/resources'), [
'term' => $query,
'page' => $page,
'limit' => 8,
'order' => $order,
]);
$items = $this->extractList($response, 'Freepik stock image search gagal');
return collect($items)->map(function (array $item) {
return [
'id' => (int) ($item['id'] ?? 0),
'kind' => 'image',
'title' => (string) ($item['title'] ?? 'Untitled asset'),
'url' => (string) ($item['url'] ?? ''),
'thumb' => (string) data_get($item, 'image.source.url', ''),
'subtitle' => trim(implode(' • ', array_filter([
strtoupper((string) data_get($item, 'image.type', 'asset')),
ucfirst((string) data_get($item, 'image.orientation', '')),
(string) data_get($item, 'author.name', ''),
]))),
'badge' => strtoupper((string) data_get($item, 'image.type', 'image')),
'meta_line' => trim(implode(' • ', array_filter([
(string) data_get($item, 'meta.published_at', ''),
'Likes '.(string) data_get($item, 'stats.likes', 0),
]))),
];
})->filter(fn (array $item) => $item['thumb'] !== '')->values()->all();
}
private function searchVideos(string $apiKey, string $query, string $order, int $page): array
{
$response = $this->client($apiKey)
->get($this->endpoint('/v1/videos'), [
'term' => $query,
'page' => $page,
'order' => $order,
]);
$items = array_slice($this->extractList($response, 'Freepik video search gagal'), 0, 8);
return collect($items)->map(function (array $item) {
return [
'id' => (int) ($item['id'] ?? 0),
'kind' => 'video',
'title' => (string) ($item['name'] ?? 'Untitled video'),
'url' => (string) ($item['url'] ?? ''),
'thumb' => (string) data_get($item, 'thumbnails.1.url', data_get($item, 'thumbnails.0.url', '')),
'preview_url' => (string) data_get($item, 'previews.0.url', ''),
'subtitle' => trim(implode(' • ', array_filter([
strtoupper((string) ($item['quality'] ?? 'video')),
(string) ($item['duration'] ?? ''),
(string) data_get($item, 'author.name', ''),
]))),
'badge' => 'VIDEO',
'meta_line' => trim(implode(' • ', array_filter([
(string) ($item['aspect_ratio'] ?? ''),
(string) ($item['item_subtype'] ?? ''),
]))),
];
})->filter(fn (array $item) => $item['thumb'] !== '')->values()->all();
}
private function searchIcons(string $apiKey, string $query, string $order, int $page): array
{
$response = $this->client($apiKey)
->get($this->endpoint('/v1/icons'), [
'term' => $query,
'page' => $page,
'per_page' => 8,
'order' => $order,
'thumbnail_size' => 256,
]);
$items = $this->extractList($response, 'Freepik icon search gagal');
return collect($items)->map(function (array $item) {
$slug = trim((string) ($item['slug'] ?? ''));
$id = (int) ($item['id'] ?? 0);
return [
'id' => $id,
'kind' => 'icon',
'title' => (string) ($item['name'] ?? 'Untitled icon'),
'url' => $slug !== '' && $id > 0 ? 'https://www.freepik.com/icon/'.$slug.'_'.$id : '',
'thumb' => (string) data_get($item, 'thumbnails.0.url', ''),
'subtitle' => trim(implode(' • ', array_filter([
(string) data_get($item, 'style.name', ''),
(string) data_get($item, 'author.name', ''),
]))),
'badge' => 'ICON',
'meta_line' => collect(data_get($item, 'tags', []))
->take(3)
->pluck('name')
->filter()
->implode(' • '),
];
})->filter(fn (array $item) => $item['thumb'] !== '')->values()->all();
}
private function searchMusic(string $apiKey, string $query, int $page): array
{
$response = $this->client($apiKey)
->get($this->endpoint('/v1/music'), [
'q' => $query,
'limit' => 6,
'offset' => ($page - 1) * 6,
]);
$json = $response->json();
if (! $response->successful()) {
throw new RuntimeException('Freepik music search gagal: '.$response->status().' - '.$this->responseError($response));
}
$items = data_get($json, 'results', []);
if (! is_array($items)) {
return [];
}
return collect($items)->map(function (array $item) {
$genres = collect(data_get($item, 'genres', []))->pluck('name')->filter()->take(2)->implode(' • ');
return [
'id' => (int) ($item['id'] ?? 0),
'kind' => 'audio',
'title' => (string) ($item['title'] ?? 'Untitled track'),
'url' => (string) ($item['preview_url'] ?? ''),
'thumb' => (string) ($item['cover_url'] ?? ''),
'subtitle' => trim(implode(' • ', array_filter([
(string) ($item['time'] ?? ''),
(string) data_get($item, 'artist.name', ''),
]))),
'badge' => 'AUDIO',
'meta_line' => trim(implode(' • ', array_filter([
$genres,
(bool) ($item['is_premium'] ?? false) ? 'Premium' : 'Free',
]))),
];
})->filter(fn (array $item) => $item['thumb'] !== '')->values()->all();
}
private function client(string $apiKey)
{
return Http::connectTimeout(15)
->timeout(45)
->retry(3, 500, fn ($exception) => $exception instanceof ConnectionException, throw: false)
->acceptJson()
->withOptions([
'force_ip_resolve' => 'v4',
])
->withHeaders([
'x-freepik-api-key' => $apiKey,
'User-Agent' => 'TeknoMedia/1.0 (+Laravel Freepik Search Client)',
]);
}
private function endpoint(string $path): string
{
return rtrim($this->apiOrigin(), '/').$path;
}
private function apiOrigin(): string
{
$configured = trim((string) config('services.freepik.base_url', 'https://api.freepik.com/v1/ai/text-to-image'));
$scheme = parse_url($configured, PHP_URL_SCHEME) ?: 'https';
$host = parse_url($configured, PHP_URL_HOST);
$port = parse_url($configured, PHP_URL_PORT);
if (! is_string($host) || $host === '') {
return 'https://api.freepik.com';
}
return $scheme.'://'.$host.($port ? ':'.$port : '');
}
private function extractList(Response $response, string $prefix): array
{
if (! $response->successful()) {
throw new RuntimeException($prefix.': '.$response->status().' - '.$this->responseError($response));
}
$items = data_get($response->json(), 'data', []);
return is_array($items) ? $items : [];
}
private function responseError(Response $response): string
{
if ($response->status() === 429) {
return 'Kuota Freepik API habis (HTTP 429). Isi billing/upgrade paket di dashboard Freepik Developers.';
}
$message = data_get($response->json(), 'message')
?? data_get($response->json(), 'error.message')
?? data_get($response->json(), 'detail')
?? $response->body();
return Str::limit(trim((string) $message), 240);
}
private function normalizeType(string $type): string
{
return match ($type) {
'image', 'images' => 'images',
'video', 'videos' => 'videos',
'audio', 'music' => 'audio',
'icon', 'icons', 'other', 'others' => 'icons',
default => 'all',
};
}
private function missingApiKeyMessage(): string
{
$value = strtolower(trim((string) (
config('services.huggingface.hf_only')
?? $_ENV['TOOLS_HF_ONLY']
?? $_SERVER['TOOLS_HF_ONLY']
?? getenv('TOOLS_HF_ONLY')
?? 'false'
)));
if (in_array($value, ['1', 'true', 'yes', 'on'], true)) {
return 'Browser stock Freepik dimatikan di mode Hugging Face-only.';
}
return 'FREEPIK_API_KEY belum diisi di file .env';
}
}