diff --git a/src/app/Console/Commands/SyncWorldHeritageVideoUrls.php b/src/app/Console/Commands/SyncWorldHeritageVideoUrls.php new file mode 100644 index 0000000..bb3e202 --- /dev/null +++ b/src/app/Console/Commands/SyncWorldHeritageVideoUrls.php @@ -0,0 +1,96 @@ +option('limit')); + $dryRun = (bool) $this->option('dry-run'); + + $first = $this->fetch(1, 0); + if ($first === null) { + return self::FAILURE; + } + + $total = (int) ($first['total_count'] ?? 0); + $offset = 0; + $updated = 0; + $skipped = 0; + + $this->info("Total UNESCO records: {$total}"); + + while ($offset < $total) { + $response = $this->fetch($limit, $offset); + if ($response === null) { + return self::FAILURE; + } + + $results = $response['results'] ?? []; + if ($results === []) { + break; + } + + foreach ($results as $row) { + $idNo = (int) ($row['id_no'] ?? 0); + $videoUrl = $row['main_video_url'] ?? null; + + if ($idNo === 0) { + $skipped++; + continue; + } + + if (!$dryRun) { + DB::table('world_heritage_sites') + ->where('id', $idNo) + ->update(['main_video_url' => $videoUrl]); + } + + $updated++; + } + + $offset += count($results); + $this->line("Processed {$offset}/{$total}..."); + } + + if ($dryRun) { + $this->info("Dry-run: would update {$updated} records, skipped {$skipped}."); + } else { + $this->info("Done. Updated {$updated} records, skipped {$skipped}."); + } + + return self::SUCCESS; + } + + private function fetch(int $limit, int $offset): ?array + { + $response = Http::acceptJson() + ->retry(3, 500) + ->get(self::API_URL, [ + 'select' => 'id_no,main_video_url', + 'limit' => $limit, + 'offset' => $offset, + 'order_by' => 'id_no asc', + ]); + + if ($response->failed()) { + $this->error("UNESCO API error: HTTP {$response->status()} offset={$offset}"); + return null; + } + + return $response->json(); + } +} \ No newline at end of file diff --git a/src/app/Models/WorldHeritage.php b/src/app/Models/WorldHeritage.php index f0722e6..85121a6 100644 --- a/src/app/Models/WorldHeritage.php +++ b/src/app/Models/WorldHeritage.php @@ -36,6 +36,7 @@ class WorldHeritage extends Model 'short_description', 'unesco_site_url', 'main_image_url', + 'main_video_url', ]; protected $hidden = [ diff --git a/src/app/Packages/Features/Controller/HeritageImageController.php b/src/app/Packages/Features/Controller/HeritageImageController.php new file mode 100644 index 0000000..06e24ab --- /dev/null +++ b/src/app/Packages/Features/Controller/HeritageImageController.php @@ -0,0 +1,74 @@ +fetchVideoUrlFromUnesco($id); + + if ($videoUrl === null) { + return response()->json(['error' => 'No video URL found for this site'], 404); + } + + $thumbnailUrl = $this->buildYoutubeThumbnailUrl($videoUrl); + + if ($thumbnailUrl === null) { + return response()->json(['error' => 'Could not extract YouTube video ID'], 422); + } + + $upstream = Http::get($thumbnailUrl); + + if ($upstream->failed()) { + return response()->json(['error' => 'Failed to fetch thumbnail from YouTube'], 502); + } + + return response($upstream->body(), 200) + ->header('Content-Type', $upstream->header('Content-Type')); + } + + private function fetchVideoUrlFromUnesco(int $idNo): ?string + { + $site = WorldHeritage::find($idNo); + + return $site?->main_video_url; + } + + private function buildYoutubeThumbnailUrl(string $videoUrl): ?string + { + if (preg_match('/(?:embed\/|v=|youtu\.be\/)([A-Za-z0-9_-]{11})/', $videoUrl, $m)) { + return "https://img.youtube.com/vi/{$m[1]}/hqdefault.jpg"; + } + + return null; + } + + public function proxyImageById(Request $request, int $imageId): Response|JsonResponse + { + $image = Image::find($imageId); + + if ($image === null) { + return response()->json(['error' => 'Image not found'], 404); + } + + $upstream = Http::withHeaders([ + 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + ])->get($image->url); + + if ($upstream->failed()) { + return response()->json(['error' => 'Failed to fetch image from upstream'], 502); + } + + return response($upstream->body(), 200) + ->header('Content-Type', $upstream->header('Content-Type')); + } +} \ No newline at end of file diff --git a/src/database/migrations/2026_05_26_232121_add_main_video_url_to_world_heritage_sites.php b/src/database/migrations/2026_05_26_232121_add_main_video_url_to_world_heritage_sites.php new file mode 100644 index 0000000..54cdd75 --- /dev/null +++ b/src/database/migrations/2026_05_26_232121_add_main_video_url_to_world_heritage_sites.php @@ -0,0 +1,22 @@ +string('main_video_url')->nullable()->after('main_image_url'); + }); + } + + public function down(): void + { + Schema::table('world_heritage_sites', function (Blueprint $table) { + $table->dropColumn('main_video_url'); + }); + } +}; \ No newline at end of file diff --git a/src/routes/api.php b/src/routes/api.php index 1d56fce..309082c 100644 --- a/src/routes/api.php +++ b/src/routes/api.php @@ -10,4 +10,5 @@ Route::get('heritages/region-count', [WorldHeritageController::class, 'getWorldHeritagesCountByRegion']); Route::get('/heritages/{id}', [WorldHeritageController::class, 'getWorldHeritageById']); Route::get('/heritage-image/{id}', [HeritageImageController::class, 'proxyImage']); + Route::get('/heritage-image/image/{imageId}', [HeritageImageController::class, 'proxyImageById']); }); \ No newline at end of file