diff --git a/classes/Config.php b/classes/Config.php index a4ff93d..72779b8 100644 --- a/classes/Config.php +++ b/classes/Config.php @@ -38,7 +38,7 @@ class Config * * @var array */ - public $params = ['--no-playlist', '--no-warnings', '-f best[protocol^=http]', '--playlist-end', 1]; + public $params = ['--no-playlist', '--no-warnings', '--playlist-end', 1]; /** * Enable audio conversion. @@ -82,6 +82,12 @@ class Config */ public $uglyUrls = false; + /** + * Stream downloaded files trough server? + * @var boolean + */ + public $stream = false; + /** * YAML config file path. * diff --git a/classes/VideoDownload.php b/classes/VideoDownload.php index 39230c9..345d2eb 100644 --- a/classes/VideoDownload.php +++ b/classes/VideoDownload.php @@ -295,4 +295,33 @@ class VideoDownload return popen($chain->getProcess()->getCommandLine(), 'r'); } + + /** + * Get video stream from an M3U playlist. + * + * @param \stdClass $video Video object returned by getJSON + * + * @return resource popen stream + */ + public function getM3uStream(\stdClass $video) + { + if (!shell_exec('which '.$this->config->avconv)) { + throw(new \Exception('Can\'t find avconv or ffmpeg')); + } + + $procBuilder = ProcessBuilder::create( + [ + $this->config->avconv, + '-v', 'quiet', + '-i', $video->url, + '-f', $video->ext, + '-c', 'copy', + '-bsf:a', 'aac_adtstoasc', + '-movflags', 'frag_keyframe+empty_moov', + 'pipe:1', + ] + ); + + return popen($procBuilder->getProcess()->getCommandLine(), 'r'); + } } diff --git a/composer.json b/composer.json index d4ed25a..40034db 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,8 @@ "symfony/process": "~3.2.0", "ptachoire/process-builder-chain": "~1.2.0", "rudloff/smarty-plugin-noscheme": "~0.1.0", + "guzzlehttp/guzzle": "~6.2.0", + "rudloff/rtmpdump-bin": "~2.3", "aura/session": "~2.1.0" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 24e660f..7bda9cb 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "15507d8a1cb225e2e118500a7883b255", + "content-hash": "44b24b403652e1a7e450280e4643e37e", "packages": [ { "name": "aura/session", @@ -95,6 +95,177 @@ "description": "Promoting the interoperability of container objects (DIC, SL, etc.)", "time": "2014-12-30T15:22:37+00:00" }, + { + "name": "guzzlehttp/guzzle", + "version": "6.2.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "ebf29dee597f02f09f4d5bbecc68230ea9b08f60" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/ebf29dee597f02f09f4d5bbecc68230ea9b08f60", + "reference": "ebf29dee597f02f09f4d5bbecc68230ea9b08f60", + "shasum": "" + }, + "require": { + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.3.1", + "php": ">=5.5" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.0", + "psr/log": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.2-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "time": "2016-10-08T15:01:37+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "v1.3.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "shasum": "" + }, + "require": { + "php": ">=5.5.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "time": "2016-12-20T10:07:11+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "5c6447c9df362e8f8093bda8f5d8873fe5c7f65b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/5c6447c9df362e8f8093bda8f5d8873fe5c7f65b", + "reference": "5c6447c9df362e8f8093bda8f5d8873fe5c7f65b", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "PSR-7 message implementation", + "keywords": [ + "http", + "message", + "stream", + "uri" + ], + "time": "2016-06-24T23:00:38+00:00" + }, { "name": "jeremykendall/php-domain-parser", "version": "3.0.0", @@ -446,6 +617,34 @@ "description": "Add ability to chain symfony processes", "time": "2016-04-10T08:33:20+00:00" }, + { + "name": "rudloff/rtmpdump-bin", + "version": "2.3", + "source": { + "type": "git", + "url": "https://github.com/Rudloff/rtmpdump-bin.git", + "reference": "133cdd80e3bab66593e88a5276158596383afd97" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Rudloff/rtmpdump-bin/zipball/133cdd80e3bab66593e88a5276158596383afd97", + "reference": "133cdd80e3bab66593e88a5276158596383afd97", + "shasum": "" + }, + "require-dev": { + "rtmpdump/rtmpdump": "2.3" + }, + "bin": [ + "rtmpdump" + ], + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0" + ], + "description": "rtmpdump binary for Linux 64 bit", + "time": "2016-04-12T19:17:32+00:00" + }, { "name": "rudloff/smarty-plugin-noscheme", "version": "0.1.1", @@ -1475,34 +1674,6 @@ }, "type": "library" }, - { - "name": "rudloff/rtmpdump-bin", - "version": "2.3", - "source": { - "type": "git", - "url": "https://github.com/Rudloff/rtmpdump-bin.git", - "reference": "133cdd80e3bab66593e88a5276158596383afd97" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Rudloff/rtmpdump-bin/zipball/133cdd80e3bab66593e88a5276158596383afd97", - "reference": "133cdd80e3bab66593e88a5276158596383afd97", - "shasum": "" - }, - "require-dev": { - "rtmpdump/rtmpdump": "2.3" - }, - "bin": [ - "rtmpdump" - ], - "type": "library", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "GPL-2.0" - ], - "description": "rtmpdump binary for Linux 64 bit", - "time": "2016-04-12T19:17:32+00:00" - }, { "name": "sebastian/code-unit-reverse-lookup", "version": "1.0.0", diff --git a/config.example.yml b/config.example.yml index 7fb80ef..6bb93e4 100644 --- a/config.example.yml +++ b/config.example.yml @@ -3,7 +3,6 @@ python: /usr/bin/python params: - --no-playlist - --no-warnings - - -f best[protocol^=http] - --playlist-end - 1 curl_params: @@ -12,3 +11,4 @@ avconv: vendor/bin/ffmpeg rtmpdump: vendor/bin/rtmpdump curl: /usr/bin/curl uglyUrls: false +stream: false diff --git a/controllers/FrontController.php b/controllers/FrontController.php index 91900c0..0c101b8 100644 --- a/controllers/FrontController.php +++ b/controllers/FrontController.php @@ -68,6 +68,11 @@ class FrontController $session_factory = new \Aura\Session\SessionFactory(); $session = $session_factory->newInstance($_COOKIE); $this->sessionSegment = $session->getSegment('Alltube\Controller\FrontController'); + if ($this->config->stream) { + $this->defaultFormat = 'best'; + } else { + $this->defaultFormat = 'best[protocol^=http]'; + } } /** @@ -156,9 +161,13 @@ class FrontController } if (isset($params['audio'])) { try { - $url = $this->download->getURL($params['url'], 'mp3[protocol^=http]', $password); + if ($this->config->stream) { + return $this->getStream($params['url'], 'mp3', $response, $request, $password); + } else { + $url = $this->download->getURL($params['url'], 'mp3[protocol^=http]', $password); - return $response->withRedirect($url); + return $response->withRedirect($url); + } } catch (PasswordException $e) { return $this->password($request, $response); } catch (\Exception $e) { @@ -178,10 +187,15 @@ class FrontController } } else { try { - $video = $this->download->getJSON($params['url'], null, $password); + $video = $this->download->getJSON($params['url'], $this->defaultFormat, $password); } catch (PasswordException $e) { return $this->password($request, $response); } + if ($this->config->stream) { + $protocol = ''; + } else { + $protocol = '[protocol^=http]'; + } $this->view->render( $response, 'video.tpl', @@ -190,6 +204,8 @@ class FrontController 'class' => 'video', 'title' => $video->title, 'description' => 'Download "'.$video->title.'" from '.$video->extractor_key, + 'protocol' => $protocol, + 'config' => $this->config, ] ); } @@ -222,6 +238,43 @@ class FrontController return $response->withStatus(500); } + /** + * Get a video/audio stream piped through the server. + * + * @param string $url URL of the video + * @param string $format Requested format + * @param Response $response PSR-7 response + * @param Request $request PSR-7 request + * @param string $password Video password + * + * @return Response + */ + private function getStream($url, $format, $response, $request, $password = null) + { + if (!isset($format)) { + $format = 'best'; + } + $video = $this->download->getJSON($url, $format, $password); + if ($video->protocol == 'm3u8') { + $stream = $this->download->getM3uStream($video); + $response = $response->withHeader('Content-Type', 'video/'.$video->ext); + if ($request->isGet()) { + $response = $response->withBody(new Stream($stream)); + } + } else { + $client = new \GuzzleHttp\Client(); + $stream = $client->request('GET', $video->url, ['stream' => true]); + $response = $response->withHeader('Content-Type', $stream->getHeader('Content-Type')); + $response = $response->withHeader('Content-Length', $stream->getHeader('Content-Length')); + if ($request->isGet()) { + $response = $response->withBody($stream->getBody()); + } + } + $response = $response->withHeader('Content-Disposition', 'attachment; filename="'.$video->_filename.'"'); + + return $response; + } + /** * Redirect to video file. * @@ -235,13 +288,23 @@ class FrontController $params = $request->getQueryParams(); if (isset($params['url'])) { try { - $url = $this->download->getURL( - $params['url'], - $request->getParam('format'), - $this->sessionSegment->getFlash($params['url']) - ); + if ($this->config->stream) { + return $this->getStream( + $params['url'], + $request->getParam('format'), + $response, + $request, + $this->sessionSegment->getFlash($params['url']) + ); + } else { + $url = $this->download->getURL( + $params['url'], + $request->getParam('format'), + $this->sessionSegment->getFlash($params['url']) + ); - return $response->withRedirect($url); + return $response->withRedirect($url); + } } catch (PasswordException $e) { return $response->withRedirect( $this->container->get('router')->pathFor('video').'?url='.urlencode($params['url']) diff --git a/package.json b/package.json index 50a9ff1..fbb66ee 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "homepage": "https://www.alltubedownload.net/", "keywords": [ "alltube", - "dowload", + "download", "video", "youtube" ], diff --git a/templates/video.tpl b/templates/video.tpl index 678ffbd..5b82363 100644 --- a/templates/video.tpl +++ b/templates/video.tpl @@ -29,18 +29,18 @@ {/if}
{else} - + Download
{/if} diff --git a/tests/VideoDownloadTest.php b/tests/VideoDownloadTest.php index 8af3275..202e858 100644 --- a/tests/VideoDownloadTest.php +++ b/tests/VideoDownloadTest.php @@ -146,7 +146,7 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase { return [ [ - 'https://www.youtube.com/watch?v=M7IpKCZ47pU', null, + 'https://www.youtube.com/watch?v=M7IpKCZ47pU', 'best[protocol^=http]', "It's Not Me, It's You - Hearts Under Fire-M7IpKCZ47pU", 'mp4', 'googlevideo.com', @@ -159,7 +159,7 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase 'googlevideo.com', ], [ - 'https://vimeo.com/24195442', null, + 'https://vimeo.com/24195442', 'best[protocol^=http]', 'Carving the Mountains-24195442', 'mp4', 'vimeocdn.com', @@ -179,6 +179,23 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase ]; } + /** + * Provides M3U8 URLs for tests. + * + * @return array[] + */ + public function M3uUrlProvider() + { + return [ + [ + 'https://twitter.com/verge/status/813055465324056576/video/1', 'best', + 'The Verge - This tiny origami robot can self-fold and complete tasks-813055465324056576', + 'mp4', + 'video.twimg.com', + ], + ]; + } + /** * Provides incorrect URLs for tests. * @@ -199,6 +216,7 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase * * @return void * @dataProvider URLProvider + * @dataProvider M3uUrlProvider */ public function testGetJSON($url, $format) { @@ -207,6 +225,7 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase $this->assertObjectHasAttribute('url', $info); $this->assertObjectHasAttribute('ext', $info); $this->assertObjectHasAttribute('title', $info); + $this->assertObjectHasAttribute('extractor_key', $info); $this->assertObjectHasAttribute('formats', $info); $this->assertObjectHasAttribute('_filename', $info); } @@ -235,6 +254,7 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase * * @return void * @dataProvider urlProvider + * @dataProvider M3uUrlProvider */ public function testGetFilename($url, $format, $filename, $extension) { @@ -267,6 +287,7 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase * * @return void * @dataProvider urlProvider + * @dataProvider M3uUrlProvider */ public function testGetAudioFilename($url, $format, $filename) { @@ -302,7 +323,7 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase */ public function testGetAudioStreamAvconvError($url, $format) { - $config = \Alltube\Config::getInstance(); + $config = Config::getInstance(); $config->avconv = 'foobar'; $this->download->getAudioStream($url, $format); } @@ -319,7 +340,7 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase */ public function testGetAudioStreamCurlError($url, $format) { - $config = \Alltube\Config::getInstance(); + $config = Config::getInstance(); $config->curl = 'foobar'; $config->rtmpdump = 'foobar'; $this->download->getAudioStream($url, $format); @@ -328,11 +349,50 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase /** * Test getAudioStream function with a M3U8 file. * + * @param string $url URL + * @param string $format Format + * * @return void * @expectedException Exception + * @dataProvider M3uUrlProvider */ - public function testGetAudioStreamM3uError() + public function testGetAudioStreamM3uError($url, $format) { - $this->download->getAudioStream('https://twitter.com/verge/status/813055465324056576/video/1', 'best'); + $this->download->getAudioStream($url, $format); + } + + /** + * Test getM3uStream function. + * + * @param string $url URL + * @param string $format Format + * + * @return void + * @dataProvider M3uUrlProvider + */ + public function testGetM3uStream($url, $format) + { + $video = $this->download->getJSON($url, $format); + $stream = $this->download->getM3uStream($video); + $this->assertInternalType('resource', $stream); + $this->assertFalse(feof($stream)); + } + + /** + * Test getM3uStream function without avconv. + * + * @param string $url URL + * @param string $format Format + * + * @return void + * @expectedException Exception + * @dataProvider M3uUrlProvider + */ + public function testGetM3uStreamAvconvError($url, $format) + { + $config = \Alltube\Config::getInstance(); + $config->avconv = 'foobar'; + $video = $this->download->getJSON($url, $format); + $this->download->getM3uStream($video); } }