use App\Services\Youtube\ChannelData;
use App\Services\Youtube\StreamData;
use App\Services\Youtube\YoutubeException;
use Carbon\Carbon;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http;
class YoutubeClient
{
public function channel(string $id): ChannelData
{
$result = $this->fetch('channels', ['id' => $id, 'part' => 'snippet'], 'items.0');
if (empty($result)) {
throw YoutubeException::unknownChannel($id);
}
return new ChannelData(
platformId: data_get($result, 'id'),
slug: data_get($result, 'snippet.customUrl'),
name: data_get($result, 'snippet.title'),
description: data_get($result, 'snippet.description'),
onPlatformSince: $this->toCarbon(data_get($result, 'snippet.publishedAt')),
thumbnailUrl: last(data_get($result, 'snippet.thumbnails'))['url'] ?? null,
country: data_get($result, 'snippet.country'),
);
}
public function upcomingStreams(string $channelId): Collection
{
$videoIds = $this->fetch('search', [
'channelId' => $channelId,
'eventType' => 'upcoming',
'type' => 'video',
'part' => 'id',
'maxResults' => 25,
], 'items.*.id.videoId');
if (empty($videoIds)) {
return collect();
}
return $this->videos($videoIds);
}
public function video(string $id): StreamData
{
return $this->videos($id)
->whenEmpty(fn() => throw YoutubeException::unknownVideo($id))
->first();
}
public function videos(iterable|string $videoIds): Collection
{
return collect($this->fetch('videos', [
'id' => is_string($videoIds) ? $videoIds : collect($videoIds)->implode(','),
'part' => 'snippet,statistics,liveStreamingDetails',
], 'items'))
->map(fn(array $item) => new StreamData(
videoId: data_get($item, 'id'),
title: data_get($item, 'snippet.title'),
channelTitle: data_get($item, 'snippet.channelTitle'),
description: data_get($item, 'snippet.description'),
thumbnailUrl: last(data_get($item, 'snippet.thumbnails'))['url'] ?? null,
publishedAt: $this->toCarbon(data_get($item, 'snippet.publishedAt')),
plannedStart: $this->getPlannedStart($item),
status: data_get($item, 'snippet.liveBroadcastContent'),
));
}
protected function getPlannedStart(array $data): Carbon
{
return $this->toCarbon(data_get($data, 'liveStreamingDetails.scheduledStartTime'))
?? $this->toCarbon(data_get($data, 'snippet.publishedAt'));
}
protected function fetch(string $url, array $params = [], string $key = null): array
{
return Http::asJson()
->baseUrl('https://youtube.googleapis.com/youtube/v3/')
->get($url, array_merge($params, [
'key' => config('services.youtube.key'),
]))
->onError(fn(Response $response) => throw YoutubeException::general($response->status()))
->json($key, []);
}
protected function toCarbon(?string $string): ?Carbon
{
if (empty($string)) {
return null;
}
return Carbon::parse($string);
}
}