<?php
/**
 * Pure PHP YouTube Downloader
 * Works without shell_exec, exec, proc_open, or cURL
 * Uses file_get_contents() to extract and download YouTube audio streams
 */

class YouTubeDownloader {
    private $videoId;
    private $videoInfo;
    
    public function __construct($videoId) {
        $this->videoId = $videoId;
    }
    
    /**
     * Create HTTP context for file_get_contents
     */
    private function createContext($url, $method = 'GET', $headers = []) {
        $defaultHeaders = [
            'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
            'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Accept-Language: en-US,en;q=0.5',
            'Accept-Encoding: gzip, deflate',
        ];
        
        $allHeaders = array_merge($defaultHeaders, $headers);
        
        $opts = [
            'http' => [
                'method' => $method,
                'header' => implode("\r\n", $allHeaders),
                'follow_location' => true,
                'max_redirects' => 5,
                'timeout' => 30,
                'ignore_errors' => false
            ],
            'ssl' => [
                'verify_peer' => false,
                'verify_peer_name' => false
            ]
        ];
        
        return stream_context_create($opts);
    }
    
    /**
     * Extract video information from YouTube
     */
    public function getVideoInfo() {
        $url = "https://www.youtube.com/watch?v=" . $this->videoId;
        
        // Get the video page using file_get_contents
        $context = $this->createContext($url);
        
        // Suppress warnings and check if allow_url_fopen is enabled
        if (!ini_get('allow_url_fopen')) {
            return false;
        }
        
        $html = @file_get_contents($url, false, $context);
        
        if ($html === false || empty($html)) {
            return false;
        }
        
        // Method 1: Extract ytInitialPlayerResponse (multi-line support with better pattern)
        // Try with semicolon
        if (preg_match('/var ytInitialPlayerResponse\s*=\s*(\{.*?\});/s', $html, $matches)) {
            $jsonStr = $matches[1];
            $jsonData = json_decode($jsonStr, true);
            if ($jsonData && isset($jsonData['videoDetails'])) {
                $this->videoInfo = $jsonData;
                return $jsonData;
            }
        }
        
        // Try without semicolon (sometimes it's not there)
        if (preg_match('/var ytInitialPlayerResponse\s*=\s*(\{.*?})(?:\s*;|\s*$)/s', $html, $matches)) {
            $jsonStr = $matches[1];
            $jsonData = json_decode($jsonStr, true);
            if ($jsonData && isset($jsonData['videoDetails'])) {
                $this->videoInfo = $jsonData;
                return $jsonData;
            }
        }
        
        // Try with escaped quotes
        if (preg_match('/var ytInitialPlayerResponse\s*=\s*(\{.*?\});/s', $html, $matches)) {
            $jsonStr = $matches[1];
            // Fix escaped quotes
            $jsonStr = str_replace('\\"', '"', $jsonStr);
            $jsonStr = str_replace('\\/', '/', $jsonStr);
            $jsonData = json_decode($jsonStr, true);
            if ($jsonData && isset($jsonData['videoDetails'])) {
                $this->videoInfo = $jsonData;
                return $jsonData;
            }
        }
        
        // Method 2: Extract from window["ytInitialPlayerResponse"]
        if (preg_match('/window\["ytInitialPlayerResponse"\]\s*=\s*({.+?});/s', $html, $matches)) {
            $jsonStr = $matches[1];
            $jsonData = json_decode($jsonStr, true);
            if ($jsonData && isset($jsonData['videoDetails'])) {
                $this->videoInfo = $jsonData;
                return $jsonData;
            }
        }
        
        // Method 3: Extract from ytInitialData (contains player response)
        if (preg_match('/var ytInitialData\s*=\s*({.+?});/s', $html, $dataMatches)) {
            $data = json_decode($dataMatches[1], true);
            if ($data) {
                // Try to find player response in the data structure
                if (isset($data['contents']['twoColumnWatchNextResults']['results']['results']['contents'][0]['videoPrimaryInfoRenderer'])) {
                    // Look for player response in various locations
                    $playerResponse = $this->findPlayerResponseInData($data);
                    if ($playerResponse) {
                        $this->videoInfo = $playerResponse;
                        return $playerResponse;
                    }
                }
            }
        }
        
        // Method 4: Try to find embedded player config (escaped JSON)
        if (preg_match('/"player_response":"({.+?})"/', $html, $matches)) {
            $jsonStr = stripslashes($matches[1]);
            $jsonStr = str_replace('\\"', '"', $jsonStr);
            $jsonData = json_decode($jsonStr, true);
            if ($jsonData && isset($jsonData['videoDetails'])) {
                $this->videoInfo = $jsonData;
                return $jsonData;
            }
        }
        
        // Method 5: Look for player_response in script tags
        if (preg_match_all('/<script[^>]*>(.*?)<\/script>/s', $html, $scriptMatches)) {
            foreach ($scriptMatches[1] as $script) {
                if (preg_match('/"player_response":\s*({.+?})/s', $script, $matches)) {
                    $jsonStr = $matches[1];
                    // Try to fix common JSON issues
                    $jsonStr = preg_replace('/,\s*}/', '}', $jsonStr); // Remove trailing commas
                    $jsonData = json_decode($jsonStr, true);
                    if ($jsonData && isset($jsonData['videoDetails'])) {
                        $this->videoInfo = $jsonData;
                        return $jsonData;
                    }
                }
            }
        }
        
        // Method 6: Search for any JSON structure containing videoDetails and streamingData
        // This is a fallback that searches the entire HTML
        if (preg_match_all('/\{[^{}]*"videoDetails"[^{}]*"streamingData"[^{}]*\}/s', $html, $fallbackMatches)) {
            foreach ($fallbackMatches[0] as $match) {
                // Try to expand to get full JSON
                $startPos = strpos($html, $match);
                if ($startPos !== false) {
                    // Try to find the complete JSON object
                    $braceCount = 0;
                    $jsonStart = $startPos;
                    for ($i = $startPos; $i < strlen($html) && $i < $startPos + 50000; $i++) {
                        if ($html[$i] === '{') $braceCount++;
                        if ($html[$i] === '}') {
                            $braceCount--;
                            if ($braceCount === 0) {
                                $jsonStr = substr($html, $jsonStart, $i - $jsonStart + 1);
                                $jsonData = json_decode($jsonStr, true);
                                if ($jsonData && isset($jsonData['videoDetails']) && isset($jsonData['streamingData'])) {
                                    $this->videoInfo = $jsonData;
                                    return $jsonData;
                                }
                                break;
                            }
                        }
                    }
                }
            }
        }
        
        return false;
    }
    
    /**
     * Recursively find player response in data structure
     */
    private function findPlayerResponseInData($data, $depth = 0) {
        if ($depth > 10) return null; // Prevent infinite recursion
        
        if (is_array($data) || is_object($data)) {
            foreach ($data as $key => $value) {
                if ($key === 'playerResponse' || $key === 'player_response') {
                    if (is_array($value) && isset($value['videoDetails'])) {
                        return $value;
                    }
                }
                if (is_array($value) || is_object($value)) {
                    $result = $this->findPlayerResponseInData($value, $depth + 1);
                    if ($result) return $result;
                }
            }
        }
        return null;
    }
    
    /**
     * Get audio stream URL
     */
    public function getAudioStreamUrl() {
        if (!$this->videoInfo) {
            if (!$this->getVideoInfo()) {
                return false;
            }
        }
        
        $streams = [];
        
        // Get adaptive formats (these contain audio-only streams)
        if (isset($this->videoInfo['streamingData']['adaptiveFormats'])) {
            foreach ($this->videoInfo['streamingData']['adaptiveFormats'] as $format) {
                if (isset($format['mimeType']) && strpos($format['mimeType'], 'audio') !== false) {
                    if (isset($format['url'])) {
                        // Direct URL available
                        $streams[] = [
                            'url' => $format['url'],
                            'bitrate' => isset($format['bitrate']) ? $format['bitrate'] : 0,
                            'mimeType' => $format['mimeType']
                        ];
                    } elseif (isset($format['signatureCipher'])) {
                        // Parse signatureCipher
                        parse_str($format['signatureCipher'], $cipherData);
                        if (isset($cipherData['url'])) {
                            $url = $cipherData['url'];
                            if (isset($cipherData['s'])) {
                                // Signature needs to be decrypted - for now, try without it
                                // Some videos work without signature
                                $url .= '&sig=' . urlencode($cipherData['s']);
                            }
                            $streams[] = [
                                'url' => $url,
                                'bitrate' => isset($format['bitrate']) ? $format['bitrate'] : 0,
                                'mimeType' => $format['mimeType']
                            ];
                        }
                    } elseif (isset($format['cipher'])) {
                        // Legacy cipher format
                        parse_str($format['cipher'], $cipherData);
                        if (isset($cipherData['url'])) {
                            $url = $cipherData['url'];
                            if (isset($cipherData['s'])) {
                                $url .= '&sig=' . urlencode($cipherData['s']);
                            }
                            $streams[] = [
                                'url' => $url,
                                'bitrate' => isset($format['bitrate']) ? $format['bitrate'] : 0,
                                'mimeType' => $format['mimeType']
                            ];
                        }
                    }
                }
            }
        }
        
        // Also check regular formats (fallback)
        if (empty($streams) && isset($this->videoInfo['streamingData']['formats'])) {
            foreach ($this->videoInfo['streamingData']['formats'] as $format) {
                if (isset($format['mimeType']) && strpos($format['mimeType'], 'audio') !== false) {
                    if (isset($format['url'])) {
                        $streams[] = [
                            'url' => $format['url'],
                            'bitrate' => isset($format['bitrate']) ? $format['bitrate'] : 0,
                            'mimeType' => $format['mimeType']
                        ];
                    }
                }
            }
        }
        
        // Sort by bitrate (highest first)
        usort($streams, function($a, $b) {
            return $b['bitrate'] - $a['bitrate'];
        });
        
        return !empty($streams) ? $streams[0]['url'] : false;
    }
    
    /**
     * Decrypt YouTube signature (simplified version)
     * Note: YouTube's signature algorithm changes frequently
     */
    private function decryptSignature($signature) {
        // This is a simplified version - YouTube's actual algorithm is more complex
        // For now, we'll try to reverse common transformations
        $sig = $signature;
        
        // Try common transformations (YouTube rotates these)
        $transformations = [
            function($s) { return strrev($s); },
            function($s) { return substr($s, 1) . $s[0]; },
            function($s) { return $s[1] . substr($s, 2) . $s[0]; },
        ];
        
        // For now, return original (this may need updating based on YouTube's current algorithm)
        return $sig;
    }
    
    /**
     * Download audio stream to file
     */
    public function downloadAudio($outputPath) {
        $audioUrl = $this->getAudioStreamUrl();
        
        if (!$audioUrl) {
            return false;
        }
        
        // Download the file using file_get_contents
        $context = $this->createContext($audioUrl);
        
        // Check if allow_url_fopen is enabled
        if (!ini_get('allow_url_fopen')) {
            return false;
        }
        
        // Try to download the file
        // First, check if URL is accessible
        $headers = @get_headers($audioUrl, 1);
        if (!$headers || (isset($headers[0]) && strpos($headers[0], '200') === false)) {
            // URL might require signature or be expired
            return false;
        }
        
        // Download the file using file_get_contents
        $audioData = @file_get_contents($audioUrl, false, $context);
        
        if ($audioData === false || empty($audioData)) {
            // Try with a simpler context (no special headers)
            $simpleContext = stream_context_create([
                'http' => [
                    'method' => 'GET',
                    'header' => 'User-Agent: Mozilla/5.0',
                    'timeout' => 60,
                    'follow_location' => true
                ],
                'ssl' => [
                    'verify_peer' => false,
                    'verify_peer_name' => false
                ]
            ]);
            $audioData = @file_get_contents($audioUrl, false, $simpleContext);
        }
        
        if ($audioData === false || empty($audioData)) {
            return false;
        }
        
        // Write to file
        $bytesWritten = @file_put_contents($outputPath, $audioData);
        
        if ($bytesWritten === false || $bytesWritten === 0) {
            @unlink($outputPath);
            return false;
        }
        
        return file_exists($outputPath) && filesize($outputPath) > 0;
    }
    
    /**
     * Get video title
     */
    public function getTitle() {
        if (!$this->videoInfo) {
            $this->getVideoInfo();
        }
        
        if (isset($this->videoInfo['videoDetails']['title'])) {
            return $this->videoInfo['videoDetails']['title'];
        }
        
        return 'YouTube Video';
    }
}

/**
 * YouTube Playlist Downloader
 * Extracts video IDs from a playlist
 */
class YouTubePlaylistDownloader {
    private $playlistId;
    
    public function __construct($playlistId) {
        $this->playlistId = $playlistId;
    }
    
    /**
     * Create HTTP context for file_get_contents
     */
    private function createContext($url, $method = 'GET', $headers = []) {
        $defaultHeaders = [
            'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
            'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Accept-Language: en-US,en;q=0.5',
            'Accept-Encoding: gzip, deflate',
        ];
        
        $allHeaders = array_merge($defaultHeaders, $headers);
        
        $opts = [
            'http' => [
                'method' => $method,
                'header' => implode("\r\n", $allHeaders),
                'follow_location' => true,
                'max_redirects' => 5,
                'timeout' => 30,
                'ignore_errors' => false
            ],
            'ssl' => [
                'verify_peer' => false,
                'verify_peer_name' => false
            ]
        ];
        
        return stream_context_create($opts);
    }
    
    /**
     * Recursively search for video IDs in array/object
     */
    private function findVideoIdsInData($data, &$videoIds = []) {
        if (is_array($data) || is_object($data)) {
            foreach ($data as $key => $value) {
                if ($key === 'videoId' && is_string($value) && preg_match('/^[a-zA-Z0-9_-]{11}$/', $value)) {
                    if (!in_array($value, $videoIds)) {
                        $videoIds[] = $value;
                    }
                } elseif (is_array($value) || is_object($value)) {
                    $this->findVideoIdsInData($value, $videoIds);
                }
            }
        }
        return $videoIds;
    }
    
    /**
     * Get all video IDs from playlist
     */
    public function getVideoIds($maxVideos = 100) {
        $videoIds = [];
        $url = "https://www.youtube.com/playlist?list=" . $this->playlistId;
        
        // Get the playlist page
        $context = $this->createContext($url);
        
        if (!ini_get('allow_url_fopen')) {
            return false;
        }
        
        $html = @file_get_contents($url, false, $context);
        
        if ($html === false || empty($html)) {
            return false;
        }
        
        // Method 1: Extract from ytInitialData (most reliable)
        if (preg_match('/var ytInitialData = ({.+?});/s', $html, $dataMatches)) {
            $jsonStr = $dataMatches[1];
            $data = json_decode($jsonStr, true);
            if ($data) {
                $this->findVideoIdsInData($data, $videoIds);
            }
        }
        
        // Method 2: Extract from ytInitialPlayerResponse
        if (empty($videoIds) && preg_match('/var ytInitialPlayerResponse = ({.+?});/s', $html, $playerMatches)) {
            $jsonStr = $playerMatches[1];
            $data = json_decode($jsonStr, true);
            if ($data) {
                $this->findVideoIdsInData($data, $videoIds);
            }
        }
        
        // Method 3: Look for videoId in JSON-like structures
        if (empty($videoIds)) {
            // Pattern for videoId in various JSON formats
            if (preg_match_all('/"videoId"\s*:\s*"([a-zA-Z0-9_-]{11})"/', $html, $matches)) {
                $videoIds = array_unique($matches[1]);
            }
        }
        
        // Method 4: Look for videoId in escaped JSON
        if (empty($videoIds)) {
            if (preg_match_all('/\\"videoId\\":\\"([a-zA-Z0-9_-]{11})\\"/', $html, $matches)) {
                $videoIds = array_merge($videoIds, $matches[1]);
                $videoIds = array_unique($videoIds);
            }
        }
        
        // Method 5: Look for watch URLs in the HTML
        if (empty($videoIds)) {
            if (preg_match_all('/watch\?v=([a-zA-Z0-9_-]{11})/', $html, $urlMatches)) {
                $videoIds = array_unique($urlMatches[1]);
            }
        }
        
        // Method 6: Try to find playlist video list renderer
        if (empty($videoIds)) {
            // Try different paths in ytInitialData
            $patterns = [
                '/"playlistVideoListRenderer":\s*\{[^}]*"contents":\s*\[(.*?)\]/s',
                '/"playlistVideoRenderer":\s*\{[^}]*"videoId":\s*"([a-zA-Z0-9_-]{11})"/',
            ];
            
            foreach ($patterns as $pattern) {
                if (preg_match_all($pattern, $html, $matches)) {
                    if (isset($matches[1])) {
                        foreach ($matches[1] as $match) {
                            if (preg_match('/"videoId":\s*"([a-zA-Z0-9_-]{11})"/', $match, $vidMatch)) {
                                $videoIds[] = $vidMatch[1];
                            }
                        }
                    }
                }
            }
            $videoIds = array_unique($videoIds);
        }
        
        // Method 7: Try extracting from embedded JSON data
        if (empty($videoIds)) {
            // Look for any JSON structure containing videoId
            if (preg_match_all('/\{[^}]*"videoId"\s*:\s*"([a-zA-Z0-9_-]{11})"[^}]*\}/', $html, $jsonMatches)) {
                foreach ($jsonMatches[1] as $vid) {
                    $videoIds[] = $vid;
                }
                $videoIds = array_unique($videoIds);
            }
        }
        
        // Remove duplicates and limit
        $videoIds = array_unique($videoIds);
        $videoIds = array_values($videoIds);
        
        // Limit to max videos
        if (count($videoIds) > $maxVideos) {
            $videoIds = array_slice($videoIds, 0, $maxVideos);
        }
        
        return !empty($videoIds) ? $videoIds : false;
    }
    
    /**
     * Get video IDs from watch page with playlist context
     */
    public function getVideoIdsFromWatchPage($videoId) {
        $videoIds = [];
        $url = "https://www.youtube.com/watch?v=" . $videoId . "&list=" . $this->playlistId;
        
        $context = $this->createContext($url);
        
        if (!ini_get('allow_url_fopen')) {
            return false;
        }
        
        $html = @file_get_contents($url, false, $context);
        
        if ($html === false || empty($html)) {
            return false;
        }
        
        // Extract from ytInitialData
        if (preg_match('/var ytInitialData = ({.+?});/s', $html, $dataMatches)) {
            $jsonStr = $dataMatches[1];
            $data = json_decode($jsonStr, true);
            if ($data) {
                $this->findVideoIdsInData($data, $videoIds);
            }
        }
        
        // Also try direct pattern matching
        if (preg_match_all('/"videoId":"([a-zA-Z0-9_-]{11})"/', $html, $matches)) {
            $videoIds = array_merge($videoIds, $matches[1]);
        }
        
        // Remove the current video ID
        $videoIds = array_values(array_filter(array_unique($videoIds), function($id) use ($videoId) {
            return $id !== $videoId;
        }));
        
        return !empty($videoIds) ? $videoIds : false;
    }
    
    /**
     * Get playlist title
     */
    public function getPlaylistTitle() {
        $url = "https://www.youtube.com/playlist?list=" . $this->playlistId;
        $context = $this->createContext($url);
        
        if (!ini_get('allow_url_fopen')) {
            return 'YouTube Playlist';
        }
        
        $html = @file_get_contents($url, false, $context);
        
        if ($html === false || empty($html)) {
            return 'YouTube Playlist';
        }
        
        // Extract playlist title
        if (preg_match('/<title>([^<]+)<\/title>/', $html, $matches)) {
            $title = trim($matches[1]);
            $title = str_replace(' - YouTube', '', $title);
            return $title;
        }
        
        return 'YouTube Playlist';
    }
}