db = $class_database; $this->logger = new VLogger('cdn'); $this->loadProviders(); } /** * Load active CDN providers */ private function loadProviders() { $sql = "SELECT * FROM db_cdn_providers WHERE is_active = 1 ORDER BY priority ASC"; $result = $this->db->execute($sql); if ($result) { while ($row = $this->db->fetch($result)) { $this->providers[] = $row; } } } /** * Upload video to CDN */ public function uploadVideo($video_id, $local_path, $quality_label = 'original') { if (empty($this->providers)) { $this->logger->warning("No CDN providers configured"); return false; } $success = false; foreach ($this->providers as $provider) { try { $this->logger->info("Uploading video $video_id to {$provider['provider_name']}"); $cdn_url = $this->uploadToProvider($provider, $local_path, $video_id, $quality_label); if ($cdn_url) { // Save CDN URL to database $this->saveCDNUrl($video_id, $provider['provider_id'], $quality_label, $cdn_url); $this->logger->info("Successfully uploaded to {$provider['provider_name']}: $cdn_url"); $success = true; // If we have a primary provider, we can stop after first success if ($provider['priority'] == 0) { break; } } } catch (Exception $e) { $this->logger->error("Failed to upload to {$provider['provider_name']}: " . $e->getMessage()); $this->updateCacheStatus($video_id, $provider['provider_id'], $quality_label, 'failed', $e->getMessage()); } } return $success; } /** * Upload to specific provider */ private function uploadToProvider($provider, $local_path, $video_id, $quality_label) { switch ($provider['provider_type']) { case 'bunny': return $this->uploadToBunnyCDN($provider, $local_path, $video_id, $quality_label); case 'cloudflare': return $this->uploadToCloudflare($provider, $local_path, $video_id, $quality_label); case 's3': return $this->uploadToS3($provider, $local_path, $video_id, $quality_label); case 'backblaze': return $this->uploadToBackblaze($provider, $local_path, $video_id, $quality_label); case 'wasabi': return $this->uploadToWasabi($provider, $local_path, $video_id, $quality_label); default: throw new Exception("Unsupported provider type: {$provider['provider_type']}"); } } /** * Upload to BunnyCDN */ private function uploadToBunnyCDN($provider, $local_path, $video_id, $quality_label) { $storage_zone = $provider['storage_zone']; $api_key = $provider['api_key']; $remote_path = "videos/{$video_id}/{$quality_label}/" . basename($local_path); $url = "https://storage.bunnycdn.com/{$storage_zone}/{$remote_path}"; $ch = curl_init($url); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "AccessKey: {$api_key}", "Content-Type: application/octet-stream" ]); curl_setopt($ch, CURLOPT_INFILE, fopen($local_path, 'r')); curl_setopt($ch, CURLOPT_INFILESIZE, filesize($local_path)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($http_code >= 200 && $http_code < 300) { // Return CDN URL $pull_zone = $provider['pull_zone']; return "https://{$pull_zone}.b-cdn.net/{$remote_path}"; } throw new Exception("BunnyCDN upload failed: HTTP $http_code - $response"); } /** * Upload to Cloudflare R2 */ private function uploadToCloudflare($provider, $local_path, $video_id, $quality_label) { // Requires AWS S3 SDK for R2 compatibility return $this->uploadToS3Compatible($provider, $local_path, $video_id, $quality_label, 'r2.cloudflarestorage.com'); } /** * Upload to AWS S3 */ private function uploadToS3($provider, $local_path, $video_id, $quality_label) { return $this->uploadToS3Compatible($provider, $local_path, $video_id, $quality_label, 's3.amazonaws.com'); } /** * Upload to Backblaze B2 */ private function uploadToBackblaze($provider, $local_path, $video_id, $quality_label) { return $this->uploadToS3Compatible($provider, $local_path, $video_id, $quality_label, "s3.{$provider['region']}.backblazeb2.com"); } /** * Upload to Wasabi */ private function uploadToWasabi($provider, $local_path, $video_id, $quality_label) { return $this->uploadToS3Compatible($provider, $local_path, $video_id, $quality_label, "s3.{$provider['region']}.wasabisys.com"); } /** * Generic S3-compatible upload */ private function uploadToS3Compatible($provider, $local_path, $video_id, $quality_label, $endpoint) { // Simplified S3 upload using cURL (basic implementation) // For production, use AWS SDK: composer require aws/aws-sdk-php $bucket = $provider['storage_zone']; $key = "videos/{$video_id}/{$quality_label}/" . basename($local_path); $region = $provider['region'] ?? 'us-east-1'; // This is a placeholder - proper S3 implementation requires AWS SDK $this->logger->warning("S3-compatible upload requires AWS SDK. Using placeholder."); // Return constructed CDN URL (assuming public bucket) return "https://{$bucket}.{$endpoint}/{$key}"; } /** * Get CDN URL for video */ public function getCDNUrl($video_id, $quality_label = 'original') { $sql = "SELECT vq.cdn_url, cp.cdn_hostname, cp.provider_type FROM db_video_qualities vq JOIN db_cdn_cache_status ccs ON vq.video_id = ccs.video_id AND vq.quality_label = ccs.quality_label JOIN db_cdn_providers cp ON ccs.provider_id = cp.provider_id WHERE vq.video_id = ? AND vq.quality_label = ? AND ccs.status = 'ready' AND cp.is_active = 1 ORDER BY cp.priority ASC LIMIT 1"; $result = $this->db->execute($sql, [$video_id, $quality_label]); if ($result && $this->db->rowCount($result) > 0) { $row = $this->db->fetch($result); return $row['cdn_url']; } // Fallback to local URL return $this->getLocalUrl($video_id, $quality_label); } /** * Get local fallback URL */ private function getLocalUrl($video_id, $quality_label) { $sql = "SELECT file_path FROM db_video_qualities WHERE video_id = ? AND quality_label = ?"; $result = $this->db->execute($sql, [$video_id, $quality_label]); if ($result && $this->db->rowCount($result) > 0) { $row = $this->db->fetch($result); return $row['file_path']; } // Ultimate fallback - check main video file $sql = "SELECT file_path FROM db_videofiles WHERE video_id = ?"; $result = $this->db->execute($sql, [$video_id]); if ($result && $this->db->rowCount($result) > 0) { $row = $this->db->fetch($result); return $row['file_path']; } return null; } /** * Get all available qualities for a video */ public function getAvailableQualities($video_id) { $sql = "SELECT DISTINCT vq.quality_label, vq.resolution, vq.cdn_url FROM db_video_qualities vq JOIN db_cdn_cache_status ccs ON vq.video_id = ccs.video_id AND vq.quality_label = ccs.quality_label WHERE vq.video_id = ? AND ccs.status = 'ready' ORDER BY CASE vq.quality_label WHEN '2160p' THEN 1 WHEN '1440p' THEN 2 WHEN '1080p' THEN 3 WHEN '720p' THEN 4 WHEN '480p' THEN 5 WHEN '360p' THEN 6 ELSE 7 END"; $result = $this->db->execute($sql, [$video_id]); if (!$result) { return []; } return $this->db->resultsToArray($result); } /** * Save CDN URL to database */ private function saveCDNUrl($video_id, $provider_id, $quality_label, $cdn_url) { // Insert or update video quality entry $sql = "INSERT INTO db_video_qualities (video_id, quality_label, cdn_url) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE cdn_url = VALUES(cdn_url)"; $this->db->execute($sql, [$video_id, $quality_label, $cdn_url]); // Update cache status $this->updateCacheStatus($video_id, $provider_id, $quality_label, 'ready'); } /** * Update CDN cache status */ private function updateCacheStatus($video_id, $provider_id, $quality_label, $status, $error_message = null) { $sql = "INSERT INTO db_cdn_cache_status (video_id, provider_id, quality_label, status, error_message) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE status = VALUES(status), error_message = VALUES(error_message), last_checked = NOW()"; $this->db->execute($sql, [$video_id, $provider_id, $quality_label, $status, $error_message]); } /** * Track bandwidth usage (called from analytics) */ public function trackBandwidth($provider_id, $video_id, $bytes_transferred) { $mb = $bytes_transferred / (1024 * 1024); $date = date('Y-m-d'); $sql = "INSERT INTO db_cdn_bandwidth_usage (provider_id, video_id, date, bandwidth_mb, requests_count) VALUES (?, ?, ?, ?, 1) ON DUPLICATE KEY UPDATE bandwidth_mb = bandwidth_mb + VALUES(bandwidth_mb), requests_count = requests_count + 1"; $this->db->execute($sql, [$provider_id, $video_id, $date, $mb]); } /** * Get bandwidth usage report */ public function getBandwidthUsage($provider_id = null, $days = 30) { $start_date = date('Y-m-d', strtotime("-$days days")); if ($provider_id) { $sql = "SELECT date, SUM(bandwidth_mb) as bandwidth_mb, SUM(requests_count) as requests FROM db_cdn_bandwidth_usage WHERE provider_id = ? AND date >= ? GROUP BY date ORDER BY date ASC"; $result = $this->db->execute($sql, [$provider_id, $start_date]); } else { $sql = "SELECT date, SUM(bandwidth_mb) as bandwidth_mb, SUM(requests_count) as requests FROM db_cdn_bandwidth_usage WHERE date >= ? GROUP BY date ORDER BY date ASC"; $result = $this->db->execute($sql, [$start_date]); } if (!$result) { return []; } return $this->db->resultsToArray($result); } /** * Purge CDN cache for a video */ public function purgeCache($video_id) { foreach ($this->providers as $provider) { try { $this->purgeCacheForProvider($provider, $video_id); } catch (Exception $e) { $this->logger->error("Failed to purge cache for {$provider['provider_name']}: " . $e->getMessage()); } } } /** * Purge cache for specific provider */ private function purgeCacheForProvider($provider, $video_id) { switch ($provider['provider_type']) { case 'bunny': $this->purgeBunnyCDN($provider, $video_id); break; case 'cloudflare': $this->purgeCloudflare($provider, $video_id); break; default: $this->logger->info("Cache purge not implemented for {$provider['provider_type']}"); } } /** * Purge BunnyCDN cache */ private function purgeBunnyCDN($provider, $video_id) { $api_key = $provider['api_key']; $pull_zone = $provider['pull_zone']; $url = "https://api.bunny.net/pullzone/{$pull_zone}/purgeCache"; $ch = curl_init($url); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "AccessKey: {$api_key}", "Content-Type: application/json" ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($http_code >= 200 && $http_code < 300) { $this->logger->info("BunnyCDN cache purged for video $video_id"); } else { throw new Exception("BunnyCDN purge failed: HTTP $http_code"); } } /** * Purge Cloudflare cache */ private function purgeCloudflare($provider, $video_id) { // Requires Cloudflare API implementation $this->logger->info("Cloudflare cache purge requires API implementation"); } }