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;
+ }
+}
+