powered by nequal
Home » Wozozo_WWW_YouTube » Timeline » 2095

Changeset 2095 -- 2010-08-26 20:53:05

Comment
[Package Release] Wozozo_WWW_YouTube

Diffs

Wozozo_WWW_YouTube/tags/release-0.0.4-20100826205305/Wozozo/WWW/YouTube.php

@@ -0,0 +1,237 @@
+<?php
+require_once 'Wozozo/WWW/YouTube/VideoInfo.php';
+
+class Wozozo_WWW_YouTube
+{
+    const PATH_INFO = 'http://www.youtube.com/get_video_info?video_id=%s';
+    const PATH_DOWNLOAD = 'http://www.youtube.com/get_video?asv=2&video_id=%s&t=%s&fmt=%s';
+
+    /**
+     * @var Zend_Http_Client
+     */
+    protected $_httpClient;
+
+    /**
+     * @var array
+     */
+    protected $_config = array('fmt' => 18,
+                               'save' => 'GETCWD', //'GETCWD' will use getcwd();
+                               'request_video_stream' => true, //output stream
+                               'response_video_cleanup' => true
+                               );
+    private $_clientStream;
+
+    public function __construct($config = null)
+    {
+        if ($config) $this->setConfig($config);
+    }
+
+    public function setConfig($config = array())
+    {
+        if ($config instanceof Zend_Config) {
+            $config = $config->toArray();
+
+        } elseif (! is_array($config)) {
+            throw new Exception('Array or Zend_Config object expected, got ' . gettype($config));
+        }
+
+        foreach ($config as $k => $v) {
+            $this->_config[strtolower($k)] = $v;
+        }
+
+        return $this;
+    }
+
+    /**
+     *
+     * @param string|Zend_Uri $videoId
+     * @param array|Zend_Config $config
+     * @param $path
+     * @return Wozozo_WWW_YouTube_VideoInfo
+     */
+    public static function download($videoId, $config = array())
+    {
+        $videoId = self::detectVideoId($videoId);
+
+        $self = new self($config);
+
+        $videoInfo = $self->getVideoInfo($videoId);
+        $self->downloadByVideoInfo($videoInfo);
+
+        return $videoInfo;
+    }
+
+    protected function _putVideo($response, Wozozo_WWW_YouTube_VideoInfo $videoInfo, $config)
+    {
+
+        if (is_string($config['save'])) {
+            $path = $this->suggestSavePath($videoInfo);
+        } else {
+
+            return call_user_func($config['save'], $response, $videoInfo, $config);
+        }
+
+        $ret = @file_put_contents($path, $response->getRawBody());
+        if ($ret === false) {
+            throw new Exception('cannot write at' . $path);
+        }
+    }
+
+    /**
+     * request & get videoinfo
+     *
+     * @param string $videoId
+     */
+    public function getVideoInfo($videoId)
+    {
+        $client = $this->getHttpClient();
+
+        $client->setUri(sprintf(self::PATH_INFO, $videoId));
+        $response = $client->request();
+
+        parse_str($response->getBody(), $parse);
+
+        return new Wozozo_WWW_YouTube_VideoInfo($parse);
+    }
+
+    /**
+     * Request video file
+     *
+     * @param Wozozo_WWW_YouTube_VideoInfo
+     * @return Zend_Http_Response_Stream
+     */
+    public function requestVideo(Wozozo_WWW_YouTube_VideoInfo $videoInfo)
+    {
+        // retrive url & save-file-path
+        $url = $videoInfo->makeDownloadUrl($this->_config['fmt']);
+
+        $client = $this->getHttpClient();
+        $client->setUri($url);
+
+        try {
+            $this->_setupClientStream();
+            $response = $client->request();
+            $this->_restoreClientStream();
+            $response->setCleanup($this->_config['response_video_cleanup']);
+
+            return $response;
+        } catch (Exception $e) {
+            // ensure, restore HttpClient's origin stream
+            $this->_restoreClientStream();
+            throw $e;
+        }
+    }
+
+    public function downloadByVideoInfo(Wozozo_WWW_YouTube_VideoInfo $videoInfo)
+    {
+        $response = $this->requestVideo($videoInfo);
+
+        $this->_putVideo($response, $videoInfo, $this->_config);
+    }
+
+    private function _setupClientStream()
+    {
+        $this->_clientStream = $this->getHttpClient()->getStream();
+        $this->getHttpClient()->setStream($this->_config['request_video_stream']);
+    }
+
+    private function _restoreClientStream()
+    {
+        $this->getHttpClient()->setStream($this->_clientStream);
+    }
+
+    public function getHttpClient()
+    {
+        if (!$this->_httpClient) {
+            require_once 'Zend/Http/Client.php';
+            $this->_httpClient = new Zend_Http_Client(null, array('useragent' => __CLASS__));
+        }
+
+        return $this->_httpClient;
+    }
+
+    public function setHttpClient(Zend_Http_Client $client)
+    {
+        $this->_httpClient = $client;
+    }
+
+    public function suggestSavePath($videoInfo)
+    {
+        $dir = $this->_config['save'];
+        $fmt = $this->_config['fmt'];
+        if ('GETCWD' ===  $dir) {
+            $dir = getcwd();
+        } else {
+            if(!is_dir($dir)) {
+                throw new InvalidArgumentException('Invalid dir'.$dir);
+            }
+        }
+        $path = $dir . DIRECTORY_SEPARATOR . $videoInfo['video_id'] . self::detectSuffix($fmt);
+
+        return $path;
+    }
+
+    /**
+     * borrowed from WWW::YouTube::Download
+     *
+     * @see
+     * http://cpansearch.perl.org/src/XAICRON/WWW-YouTube-Download-0.13/lib/WWW/YouTube/Download.pm
+     *
+     * @param string
+     * @return string
+     */
+    public static function detectSuffix($fmt)
+    {
+        switch ($fmt) {
+            case '18' :
+            case '22' :
+            case '37' :
+                return '.mp4';
+            case '13' :
+            case '17' :
+                return '.3gp';
+            default :
+                return '.flv';
+        }
+    }
+
+    /**
+     * detect videoId
+     *
+     * @param string $var (url)
+     * @return string|false
+     */
+    public static function detectVideoId($var)
+    {
+        if (is_string($var)) {
+            if (!preg_match('#^h*(?:ttp\:\/\/)(.+\/watch\?v=.*)#', $var, $match)) {
+                if (!preg_match('#^[A-Za-z0-9]+$#', $var)) {
+                    throw new InvalidArgumentException('Invalid id '.$var);
+                }
+                return trim($var);
+            }
+            //uri
+            require_once 'Zend/Uri.php';
+            $var = Zend_Uri::factory('http://'.$match[1]);
+        }
+
+        if ($var instanceof Zend_Uri_Http) {
+            $query = $var->getQueryAsArray();
+            return $query['v'];
+        }
+
+        return false;
+    }
+
+    /*
+    public function __destruct()
+    {
+        if (is_string($s = $this->_config['output_stream']) && $this->_config['download_response_cleanup']) {
+            if (file_exists($s)) {
+                unlink($s);
+            }
+        }
+    }
+    */
+}
+

Wozozo_WWW_YouTube/tags/release-0.0.4-20100826205305/Wozozo/WWW/YouTube/Storage/Couchdb.php

@@ -0,0 +1,52 @@
+<?php
+require_once 'Sopha/Db.php';
+
+class Wozozo_WWW_YouTube_Storage_Couchdb
+{
+    private $_document;
+    private $_url;
+    private $_name = 'video';
+
+    public function __construct($videoInfo, $url, $dbname, $host = 'localhost', $port = Sopha_Db::COUCH_PORT)
+    {
+        $db = new Sopha_Db($dbname, $host, $port);
+
+        $this->_validateExists($url, $db);
+
+        $this->_document = new Sopha_Document($videoInfo->toArray(), $url, $db);
+
+        $this->_url = $db->getUrl().urlencode($url).'/'.$this->_name;
+    }
+
+    public function setAttachmentName($name)
+    {
+        $this->_name = $name;
+    }
+
+    public function getUrl()
+    {
+        //return $this->_document->getUrl();
+        return $this->_url;
+    }
+
+    private function _validateExists($url, $db)
+    {
+        if ($db->retrieve($url)) {
+            throw new Exception($url . 'is already used.');
+        }
+    }
+
+    //save
+    public function callbackUpdate($response, $videoInfo, $config)
+    {
+        $document = $this->_document;
+        $document->save();
+
+        $type = 'video/x-flv';
+        $document->setAttachment($this->_name, $type, $response->getRawBody());
+
+        $document->save();
+    }
+
+}
+

Wozozo_WWW_YouTube/tags/release-0.0.4-20100826205305/Wozozo/WWW/YouTube/HttpSocketProgressBar.php

@@ -0,0 +1,232 @@
+<?php
+
+require_once 'Zend/Http/Client/Adapter/Socket.php';
+class Wozozo_WWW_YouTube_HttpSocketProgressBar extends Zend_Http_Client_Adapter_Socket
+{
+    private $_max = 0;
+    private $_progressBar;
+
+    public function setMax($max)
+    {
+        $this->_max = $max;
+    }
+
+    public function getMax()
+    {
+        return $this->_max;
+    }
+
+    private function getProgressBar()
+    {
+        if (PHP_SAPI != 'cli') throw new RuntimeException();
+
+        if (!$this->_progressBar) {
+            require_once 'Zend/ProgressBar/Adapter/Console.php';
+            require_once 'Zend/ProgressBar.php';
+            $adapter = new Zend_ProgressBar_Adapter_Console();
+            $this->_progressBar = new Zend_ProgressBar($adapter, 0, $this->getMax());
+        }
+
+        return $this->_progressBar;
+
+    }
+
+    /**
+     * Read response from server
+     *
+     * @return string
+     */
+    public function read()
+    {
+        // First, read headers only
+        $response = '';
+        $gotStatus = false;
+        $stream = !empty($this->config['stream']);
+
+        while (($line = @fgets($this->socket)) !== false) {
+            $gotStatus = $gotStatus || (strpos($line, 'HTTP') !== false);
+            if ($gotStatus) {
+                $response .= $line;
+                if (rtrim($line) === '') break;
+            }
+        }
+
+        $this->_checkSocketReadTimeout();
+
+        $statusCode = Zend_Http_Response::extractCode($response);
+
+        // Handle 100 and 101 responses internally by restarting the read again
+        if ($statusCode == 100 || $statusCode == 101) return $this->read();
+
+        // Check headers to see what kind of connection / transfer encoding we have
+        $headers = Zend_Http_Response::extractHeaders($response);
+
+        /**
+         * Responses to HEAD requests and 204 or 304 responses are not expected
+         * to have a body - stop reading here
+         */
+        if ($statusCode == 304 || $statusCode == 204 ||
+            $this->method == Zend_Http_Client::HEAD) {
+
+            // Close the connection if requested to do so by the server
+            if (isset($headers['connection']) && $headers['connection'] == 'close') {
+                $this->close();
+            }
+            return $response;
+        }
+
+        // If we got a 'transfer-encoding: chunked' header
+        if (isset($headers['transfer-encoding'])) {
+
+            if (strtolower($headers['transfer-encoding']) == 'chunked') {
+
+                do {
+                    $line  = @fgets($this->socket);
+                    $this->_checkSocketReadTimeout();
+
+                    $chunk = $line;
+
+                    // Figure out the next chunk size
+                    $chunksize = trim($line);
+                    if (! ctype_xdigit($chunksize)) {
+                        $this->close();
+                        require_once 'Zend/Http/Client/Adapter/Exception.php';
+                        throw new Zend_Http_Client_Adapter_Exception('Invalid chunk size "' .
+                            $chunksize . '" unable to read chunked body');
+                    }
+
+                    // Convert the hexadecimal value to plain integer
+                    $chunksize = hexdec($chunksize);
+
+                    // Read next chunk
+                    $read_to = ftell($this->socket) + $chunksize;
+
+                    do {
+                        $current_pos = ftell($this->socket);
+                        if ($current_pos >= $read_to) break;
+
+                        if($this->out_stream) {
+                            if(stream_copy_to_stream($this->socket, $this->out_stream, $read_to - $current_pos) == 0) {
+                              $this->_checkSocketReadTimeout();
+                              break;
+                             }
+                        } else {
+                            $line = @fread($this->socket, $read_to - $current_pos);
+                            if ($line === false || strlen($line) === 0) {
+                                $this->_checkSocketReadTimeout();
+                                break;
+                            }
+                                    $chunk .= $line;
+                        }
+                    } while (! feof($this->socket));
+
+                    $chunk .= @fgets($this->socket);
+                    $this->_checkSocketReadTimeout();
+
+                    if(!$this->out_stream) {
+                        $response .= $chunk;
+                    }
+                } while ($chunksize > 0);
+            } else {
+                $this->close();
+                throw new Zend_Http_Client_Adapter_Exception('Cannot handle "' .
+                    $headers['transfer-encoding'] . '" transfer encoding');
+            }
+
+            // We automatically decode chunked-messages when writing to a stream
+            // this means we have to disallow the Zend_Http_Response to do it again
+            if ($this->out_stream) {
+                $response = str_ireplace("Transfer-Encoding: chunked\r\n", '', $response);
+            }
+        // Else, if we got the content-length header, read this number of bytes
+        } elseif (isset($headers['content-length'])) {
+
+            // If we got more than one Content-Length header (see ZF-9404) use
+            // the last value sent
+            if (is_array($headers['content-length'])) {
+                $contentLength = $headers['content-length'][count($headers['content-length']) - 1];
+            } else {
+                $contentLength = $headers['content-length'];
+            }
+
+            if ($contentLength != 0) $this->setMax($contentLength);
+
+            $current_pos = ftell($this->socket);
+            $chunk = '';
+
+            $startProgressBar = false;
+            for ($read_to = $current_pos + $contentLength;
+                 $read_to > $current_pos;
+                 $current_pos = ftell($this->socket)) {
+
+                 if ($startProgressBar === false) {
+                     $this->setMax($read_to);
+                     $startProgressBar = true;
+                 }
+                 $this->getProgressBar()->update($current_pos);
+
+                 //if($this->out_stream) {
+
+                     /*
+                     if(@stream_copy_to_stream($this->socket, $this->out_stream, $read_to - $current_pos) == 0) {
+                          $this->_checkSocketReadTimeout();
+                          break;
+                     }
+                     */
+                 //} else {
+                    $chunk = @fread($this->socket, $read_to - $current_pos);
+                    if ($chunk === false || strlen($chunk) === 0) {
+                        $this->_checkSocketReadTimeout();
+                        break;
+                    }
+
+                    if ($this->out_stream) {
+                         $fwrite = fwrite($this->out_stream, $chunk);
+                         if ($fwrite === false) {
+                             $this->close();
+                             throw new Exception('cannot write stream');
+                         }
+                    } else {
+                        $response .= $chunk;
+                    }
+                //}
+
+                // Break if the connection ended prematurely
+                if (feof($this->socket)) break;
+            }
+            if ($startProgressBar) $this->getProgressBar()->finish();
+
+        // Fallback: just read the response until EOF
+        } else {
+
+            do {
+                if($this->out_stream) {
+                    if(@stream_copy_to_stream($this->socket, $this->out_stream) == 0) {
+                          $this->_checkSocketReadTimeout();
+                          break;
+                     }
+                }  else {
+                    $buff = @fread($this->socket, 8192);
+                    if ($buff === false || strlen($buff) === 0) {
+                        $this->_checkSocketReadTimeout();
+                        break;
+                    } else {
+                        $response .= $buff;
+                    }
+                }
+
+            } while (feof($this->socket) === false);
+
+            $this->close();
+        }
+
+        // Close the connection if requested to do so by the server
+        if (isset($headers['connection']) && $headers['connection'] == 'close') {
+            $this->close();
+        }
+
+        return $response;
+    }
+
+}
+

Wozozo_WWW_YouTube/tags/release-0.0.4-20100826205305/Wozozo/WWW/YouTube/VideoInfo.php

@@ -0,0 +1,109 @@
+<?php
+
+class Wozozo_WWW_YouTube_VideoInfo implements ArrayAccess
+{
+
+    const FORMAT_MP4 = 'mp4';
+    const FORMAT_FLV = 'flv';
+
+    private $_parsedVideoInfo;
+
+    public function __construct(array $parsedVideoInfo)
+    {
+        if (!isset($parsedVideoInfo['status'])) {
+            $msg = implode('', $parsedVideoInfo);
+            throw new UnexpectedValueException("invalid array is passed, array has not 'status' - key");
+        }
+
+        $this->_parsedVideoInfo = $parsedVideoInfo;
+    }
+
+    public function makeDownloadUrl($fmt = 18)
+    {
+        if ($this->_parsedVideoInfo['status'] !== 'ok') {
+            if ($this->_parsedVideoInfo['status'] === 'fail') {
+                throw new Exception($this->_parsedVideoInfo['reason'], $this->_parsedVideoInfo['errorcode']);
+            } else {
+                throw new Exception('error raise by unknown status'.$this->_parsedVideoInfo['status']);
+            }
+        }
+
+        // get detected high quality fmt
+        $fmt = (string) $this->_highFmt($fmt);
+
+        $videoId = $this->_parsedVideoInfo['video_id'];
+        $token = $this->_parsedVideoInfo['token'];
+
+        return sprintf(Wozozo_WWW_YouTube::PATH_DOWNLOAD, $videoId, $token, $fmt);
+    }
+
+    protected function _highFmt($fmt)
+    {
+        if (is_integer($fmt)) {
+            return $fmt;
+        }
+
+        //} elseif ($fmt === self::FORMAT_HIGH) {
+        $fmts  = $this->getFmts();
+        rsort($fmts);
+        $mp4s = array(37, 22, 18, 17, 13);
+        if (self::FORMAT_MP4 === $fmt) {
+            $intersect = array_intersect($fmts, $mp4s);
+            return current($intersect);
+        } else {
+            $diff = array_diff($fmts, $mp4s);
+            return current($diff);
+        }
+    }
+
+    /**
+     * Get format list
+     *  fmt_list or fmt_map
+     *  eg. 22/2000000/9/0/115,35/640000/9/0/115,34/0/9/0/115,5/0/7/0/0"
+     *
+     * @see http://kenz0.s201.xrea.com/weblog/2008/11/youtube_9.html
+     * @see http://creazy.net/2008/12/youtube_video_format_list.html
+     *
+     * @return array
+     */
+    public function getFmts()
+    {
+        if (preg_match_all('#(?:^|,)([\d]+)#', $this->offsetGet('fmt_list'), $m)) {
+            return $m[1];
+        } else {
+            throw new Exception('extract fmt_list error');
+        }
+
+    }
+
+    public function offsetExists($offset)
+    {
+        return isset($this->_parsedVideoInfo[$offset]);
+    }
+
+    public function offsetGet($offset)
+    {
+        return $this->_parsedVideoInfo[$offset];
+    }
+
+    public function offsetSet($offset, $value)
+    {
+        $this->_parsedVideoInfo[$offset] = $value;
+    }
+
+    public function offsetUnset($offset)
+    {
+        unset($this->_parsedVideoInfo[$offset]);
+    }
+
+    public function toArray()
+    {
+        return $this->_parsedVideoInfo;
+    }
+
+    public function __toString()
+    {
+        return http_build_query($this->_parsedVideoInfo);
+    }
+
+}

Wozozo_WWW_YouTube/tags/release-0.0.4-20100826205305/Wozozo/WWW/YouTube/Tool/YoutubeProvider.php

@@ -0,0 +1,125 @@
+<?php
+require_once 'Wozozo/WWW/YouTube.php';
+require_once 'Zend/Tool/Framework/Provider/Abstract.php';
+
+class Wozozo_WWW_YouTube_Tool_YoutubeProvider extends Zend_Tool_Framework_Provider_Abstract
+{
+    protected $_specialties = array('SendQueue', 'Couchdb');
+
+    /**
+     * @var Wozozo_WWW_YouTube
+     */
+    protected $_youtube;
+
+    public function getDownloadUrl($id)
+    {
+        $videoId = Wozozo_WWW_YouTube::detectVideoId($id);
+        $youtube = $this->_loadYoutube();
+        $url = $youtube->getVideoInfo($videoId)->makeDownloadUrl();
+
+        $this->_out($url);
+    }
+
+    public function download($id, $path = 'GETCWD')
+    {
+        $this->_download($id, $path, false);
+    }
+
+    public function downloadCouchdb($id)
+    {
+        $this->_download($id, null, true);
+    }
+
+    private function _setupSave($videoInfo, $path, $couch = false)
+    {
+        if ($couch) {
+            $config = $this->_loadConfig('couchdb');
+            if (!isset($config->dbname)) {
+                throw new Exception ('should config dbname');
+            }
+            if ($dbname = $config->dbname) {
+                require_once 'Wozozo/WWW/YouTube/Storage/Couchdb.php';
+                $storage = new Wozozo_WWW_YouTube_Storage_Couchdb($videoInfo, $videoInfo['video_id'], $dbname);
+                $this->_youtube->setConfig(array('save' => array($storage, 'callbackUpdate')));
+
+                return $storage->getUrl();
+            }
+        } else {
+            $this->_youtube->setConfig(array('save' => $path));
+            return $this->_youtube->suggestSavePath($videoInfo);
+        }
+    }
+
+    protected function _download($id, $save, $couch = false)
+    {
+        $videoId = Wozozo_WWW_YouTube::detectVideoId($id);
+        $this->_out("Video ID :$videoId");
+
+        $youtube = $this->_loadYoutube();
+        $videoInfo = $youtube->getVideoInfo($videoId);
+        $this->_out("Status :". $videoInfo['status']);
+
+        if ($videoInfo['status'] != 'ok') {
+            $videoInfo = $videoInfo->toArray();
+            $message = '';
+            array_walk($videoInfo, function($v, $k) use (&$message){$message .= $k .' : '. $v .PHP_EOL;});
+            throw new Exception("Status is not ok ". PHP_EOL .$message);
+        }
+
+        $this->_out("Title : ". $videoInfo['title']);
+        $this->_out("Length Seconds : ". $videoInfo['length_seconds']);
+
+        $client = $youtube->getHttpClient();
+        //ensure load adapter
+        $client->setAdapter('Wozozo_WWW_YouTube_HttpSocketProgressBar');
+
+        $youtube->setHttpClient($client);
+        /*
+        if ($path) $youtube->setConfig(array('save' => $path));
+        $path = $youtube->suggestSavePath($videoInfo);
+        */
+        $path = $this->_setupSave($videoInfo, $save, $couch);
+
+        $this->_out("Downloading ..: ". $path);
+        $youtube->downloadByVideoInfo($videoInfo);
+    }
+
+    //@todo
+    //public function downloadSendQueue()
+    //{}
+
+    protected function _loadYoutube()
+    {
+        if (null === $this->_youtube) {
+            $youtube = new Wozozo_WWW_YouTube();
+            if ($config = $this->_loadConfig('youtube')) {
+                if ($config->httpClient) {
+                    require_once 'Zend/Http/Client.php';
+                    $client = new Zend_Http_Client(null, $config->httpClient);
+                    $youtube->setHttpClient($client);
+                }
+                $youtube->setConfig($config);
+            }
+
+            $this->_youtube = $youtube;
+        }
+
+        return $this->_youtube;
+    }
+
+    protected function _loadConfig($key)
+    {
+        $userConfig = $this->_registry->getConfig();
+
+        return $userConfig->$key;
+    }
+
+    protected function _out($string)
+    {
+        //if ()
+        $this->_registry->getResponse()->appendContent($string);
+
+        //echo $string, PHP_EOL;
+    }
+}
+