From e18c8bbbcffe051a8a4b3d413daebdacbe363397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Wed, 20 May 2026 22:55:07 +0300 Subject: [PATCH] feat: update YouTube API integration with enhanced release date parsing, improved topic channel resolution, and localization updates for music productions --- .env.example | 5 +- app/Console/Commands/SyncYoutubeReleases.php | 15 ++-- app/Services/YouTubeService.php | 81 ++++++++++++++++++- lang/en/music_productions.php | 2 + lang/tr/music_productions.php | 2 + .../front/music-productions/index.blade.php | 77 +++++++----------- 6 files changed, 121 insertions(+), 61 deletions(-) diff --git a/.env.example b/.env.example index 82d98f5..d2347de 100644 --- a/.env.example +++ b/.env.example @@ -74,9 +74,8 @@ SPOTIFY_MARKET=TR # YouTube Data API v3 (Müzik Prodüksiyonları / Art Track senkronizasyonu) # Ana kanal (@handle) — Videolar sekmesi YOUTUBE_CHANNEL_ID= -# Topic kanalı — Yayınlananlar sekmesi (DistroKid art track). Boş bırakılırsa otomatik bulunur. -# Örnek: Ümit Reva kanalı için UCEGzDgiExoGrwWEnpIdOGRA -YOUTUBE_TOPIC_CHANNEL_ID= +# Topic kanalı — Yayınlananlar sekmesi (ZORUNLU önerilir, otomatik arama güvenilir değil) +YOUTUBE_TOPIC_CHANNEL_ID=UCEGzDgiExoGrwWEnpIdOGRA # Sunucu/cron senkronizasyonu için KISITLAMASIZ veya IP kısıtlı ayrı bir anahtar kullanın. YOUTUBE_API_KEY= # İsteğe bağlı: playlist ID doğrudan (Topic uploads: UU + topic channel ID'den sonraki kısım) diff --git a/app/Console/Commands/SyncYoutubeReleases.php b/app/Console/Commands/SyncYoutubeReleases.php index e5f7c5b..2bc7e5a 100644 --- a/app/Console/Commands/SyncYoutubeReleases.php +++ b/app/Console/Commands/SyncYoutubeReleases.php @@ -67,8 +67,8 @@ class SyncYoutubeReleases extends Command } $title = $release['title'] ?? __('music_productions.youtube_untitled_release'); - $releaseDate = filled($release['published_at'] ?? null) - ? Carbon::parse($release['published_at']) + $releaseDate = filled($release['release_date'] ?? null) + ? Carbon::parse($release['release_date']) : null; $youtubeUrl = $release['youtube_url'] ?? null; $coverUrl = $release['cover_url'] ?? null; @@ -121,7 +121,7 @@ class SyncYoutubeReleases extends Command $attributes['title'] = $title; } - if (blank($production->production_date) && $releaseDate) { + if ($releaseDate) { $attributes['production_date'] = $releaseDate; } @@ -171,11 +171,16 @@ class SyncYoutubeReleases extends Command $slug = Str::slug($title); - $bySlug = MusicProduction::where('slug', '=', $slug, 'and')->first(); + $bySlug = MusicProduction::whereNull('youtube_video_id') + ->where('slug', '=', $slug, 'and') + ->first(); + if ($bySlug) { return $bySlug; } - return MusicProduction::whereRaw('LOWER(title) = ?', [Str::lower($title)], 'and')->first(); + return MusicProduction::whereNull('youtube_video_id') + ->whereRaw('LOWER(title) = ?', [Str::lower($title)], 'and') + ->first(); } } diff --git a/app/Services/YouTubeService.php b/app/Services/YouTubeService.php index 010c93b..1fb5cb6 100644 --- a/app/Services/YouTubeService.php +++ b/app/Services/YouTubeService.php @@ -7,6 +7,7 @@ use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; +use Carbon\Carbon; class YouTubeService { @@ -168,13 +169,14 @@ class YouTubeService } $expectedTopicTitle = $artistTitle . ' - Topic'; + $mainThumbnail = $channel['snippet']['thumbnails']['default']['url'] ?? null; $response = $this->client()->get('https://www.googleapis.com/youtube/v3/search', [ 'key' => $this->apiKey, 'part' => 'snippet', 'type' => 'channel', 'q' => $expectedTopicTitle, - 'maxResults' => 5, + 'maxResults' => 10, ]); if ($response->failed()) { @@ -187,22 +189,77 @@ class YouTubeService return null; } + $topicCandidates = []; + foreach ($response->json('items') ?? [] as $item) { $channelId = $item['snippet']['channelId'] ?? $item['id']['channelId'] ?? null; $title = trim($item['snippet']['title'] ?? ''); - if (blank($channelId)) { + if (blank($channelId) || $channelId === $mainChannelId) { continue; } - if ($title === $expectedTopicTitle || str_ends_with($title, ' - Topic')) { + if ($this->titlesMatch($title, $expectedTopicTitle)) { + Log::info('YouTube topic channel resolved by exact title match', [ + 'main_channel_id' => $mainChannelId, + 'topic_channel_id' => $channelId, + 'title' => $title, + ]); + return $channelId; } + + if (str_ends_with($title, ' - Topic')) { + $topicCandidates[] = [ + 'channel_id' => $channelId, + 'title' => $title, + 'thumbnail' => $item['snippet']['thumbnails']['default']['url'] ?? null, + ]; + } } + foreach ($topicCandidates as $candidate) { + if ($this->thumbnailsMatch($mainThumbnail, $candidate['thumbnail'])) { + Log::info('YouTube topic channel resolved by avatar match', [ + 'main_channel_id' => $mainChannelId, + 'topic_channel_id' => $candidate['channel_id'], + 'title' => $candidate['title'], + ]); + + return $candidate['channel_id']; + } + } + + Log::warning('YouTube topic channel could not be resolved safely', [ + 'main_channel_id' => $mainChannelId, + 'expected_title' => $expectedTopicTitle, + 'candidates' => collect($topicCandidates)->pluck('title')->all(), + ]); + return null; } + protected function titlesMatch(string $left, string $right): bool + { + return mb_strtolower(trim($left)) === mb_strtolower(trim($right)); + } + + protected function thumbnailsMatch(?string $left, ?string $right): bool + { + if (blank($left) || blank($right)) { + return false; + } + + return $this->normalizeThumbnailUrl($left) === $this->normalizeThumbnailUrl($right); + } + + protected function normalizeThumbnailUrl(string $url): string + { + $path = parse_url($url, PHP_URL_PATH) ?: $url; + + return preg_replace('/=s\d+-/', '=s88-', $path) ?? $path; + } + /** * @return array|null */ @@ -271,10 +328,28 @@ class YouTubeService 'description' => $description, 'cover_url' => $coverUrl, 'published_at' => $snippet['publishedAt'] ?? null, + 'release_date' => $this->parseReleaseDate($description, $snippet['publishedAt'] ?? null), 'youtube_url' => 'https://www.youtube.com/watch?v=' . $videoId, ]; } + public function parseReleaseDate(string $description, ?string $publishedAt = null): ?string + { + if (preg_match('/Released on:\s*(\d{4}-\d{2}-\d{2})/i', $description, $matches)) { + return Carbon::parse($matches[1])->toDateString(); + } + + if (preg_match('/Release date:\s*(\d{4}-\d{2}-\d{2})/i', $description, $matches)) { + return Carbon::parse($matches[1])->toDateString(); + } + + if (filled($publishedAt)) { + return Carbon::parse($publishedAt)->toDateString(); + } + + return null; + } + public function downloadCoverImage(?string $imageUrl, string $videoId): ?string { if (blank($imageUrl)) { diff --git a/lang/en/music_productions.php b/lang/en/music_productions.php index 65ae969..8426fc3 100644 --- a/lang/en/music_productions.php +++ b/lang/en/music_productions.php @@ -6,6 +6,8 @@ return [ 'meta-index-title' => 'Music Productions', 'meta-index-description' => 'Our music production, soundtrack and corporate music work.', 'empty' => 'No music productions registered yet.', + 'card_category_default' => 'Production', + 'view_details_tooltip' => 'View details', 'project_details' => 'Project Details', 'back_to_list' => 'Back to List', 'prev' => 'Prev', diff --git a/lang/tr/music_productions.php b/lang/tr/music_productions.php index a9460d3..47401f9 100644 --- a/lang/tr/music_productions.php +++ b/lang/tr/music_productions.php @@ -6,6 +6,8 @@ return [ 'meta-index-title' => 'Müzik Prodüksiyonları', 'meta-index-description' => 'Müzik prodüksiyonu, film müzikleri ve kurumsal müzik çalışmalarımız.', 'empty' => 'Şu anda kayıtlı müzik prodüksiyonu bulunmuyor.', + 'card_category_default' => 'Prodüksiyon', + 'view_details_tooltip' => 'Detayları görüntüle', 'project_details' => 'Proje Detayları', 'back_to_list' => 'Listeye Geri Dön', 'prev' => 'Önceki', diff --git a/resources/views/front/music-productions/index.blade.php b/resources/views/front/music-productions/index.blade.php index 0f3d04e..369a179 100644 --- a/resources/views/front/music-productions/index.blade.php +++ b/resources/views/front/music-productions/index.blade.php @@ -15,31 +15,33 @@
@if($productions->count() > 0) -
-
+
+
@php $accentColors = [ - ['border' => '#54a8c7', 'text' => '#54a8c7', 'tooltip' => 'itooltip-aqua'], - ['border' => '#747ed1', 'text' => '#747ed1', 'tooltip' => 'itooltip-purple'], - ['border' => '#fab758', 'text' => '#fab758', 'tooltip' => 'itooltip-yellow'], - ['border' => '#e2626b', 'text' => '#e2626b', 'tooltip' => 'itooltip-red'], - ['border' => '#f78b77', 'text' => '#f78b77', 'tooltip' => 'itooltip-orange'], - ['border' => '#7cb798', 'text' => '#7cb798', 'tooltip' => 'itooltip-leaf'], - ['border' => '#d16b86', 'text' => '#d16b86', 'tooltip' => 'itooltip-pink'], - ['border' => '#45c4a0', 'text' => '#45c4a0', 'tooltip' => 'itooltip-green'], - ['border' => '#a07cc5', 'text' => '#a07cc5', 'tooltip' => 'itooltip-violet'], + ['border' => '#54a8c7', 'text' => '#54a8c7', 'tooltip' => 'itooltip-aqua', 'filter' => 'workshop'], + ['border' => '#747ed1', 'text' => '#747ed1', 'tooltip' => 'itooltip-purple', 'filter' => 'product'], + ['border' => '#fab758', 'text' => '#fab758', 'tooltip' => 'itooltip-yellow', 'filter' => 'product'], + ['border' => '#e2626b', 'text' => '#e2626b', 'tooltip' => 'itooltip-red', 'filter' => 'workshop'], + ['border' => '#f78b77', 'text' => '#f78b77', 'tooltip' => 'itooltip-orange', 'filter' => 'still-life'], + ['border' => '#7cb798', 'text' => '#7cb798', 'tooltip' => 'itooltip-leaf', 'filter' => 'concept'], + ['border' => '#d16b86', 'text' => '#d16b86', 'tooltip' => 'itooltip-pink', 'filter' => 'product'], + ['border' => '#45c4a0', 'text' => '#45c4a0', 'tooltip' => 'itooltip-green', 'filter' => 'workshop'], + ['border' => '#a07cc5', 'text' => '#a07cc5', 'tooltip' => 'itooltip-violet', 'filter' => 'concept'], ]; @endphp @foreach($productions as $index => $production) @php $color = $accentColors[$index % count($accentColors)]; + $categoryLabel = $production->translate('client_name') ?: __('music_productions.card_category_default'); + $detailUrl = route('music-productions.show', $production->slug); @endphp -
- + {{-- Pagination --}} @if($productions->hasPages()) @@ -149,26 +146,6 @@ cursor: not-allowed; } -/* Masonry item entrance animation */ -.project.item { - animation: fadeInUp 0.6s ease forwards; - opacity: 0; -} -@keyframes fadeInUp { - from { - opacity: 0; - transform: translateY(30px); - } - to { - opacity: 1; - transform: translateY(0); - } -} -@for($i = 0; $i < 9; $i++) -.project.item:nth-child({{ $i + 1 }}) { - animation-delay: {{ $i * 0.1 }}s; -} -@endfor @endpush @endsection