Merge branch 'feature/stream' into develop

This commit is contained in:
Pierre Rudloff 2017-01-16 12:02:40 +01:00
commit 9fed42d648
9 changed files with 382 additions and 51 deletions

View file

@ -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.
*

View file

@ -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');
}
}

View file

@ -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": {

229
composer.lock generated
View file

@ -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",

View file

@ -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

View file

@ -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'])

View file

@ -27,7 +27,7 @@
"homepage": "https://www.alltubedownload.net/",
"keywords": [
"alltube",
"dowload",
"download",
"video",
"youtube"
],

View file

@ -29,18 +29,18 @@
{/if}
<select name="format" id="format" class="formats monospace">
<optgroup label="Generic formats">
<option value="best[protocol^=http]">
<option value="best{$protocol}">
{strip}
Best ({$video->ext})
{/strip}
</option>
<option value="worst[protocol^=http]">
<option value="worst{$protocol}">
Worst
</option>
</optgroup>
<optgroup label="Detailed formats" class="monospace">
{foreach $video->formats as $format}
{if $format->protocol|in_array:array('http', 'https')}
{if $config->stream || $format->protocol|in_array:array('http', 'https')}
{strip}
<option value="{$format->format_id}">
{$format->ext}
@ -80,7 +80,7 @@
<input class="downloadBtn" type="submit" value="Download" /><br/>
</form>
{else}
<input type="hidden" name="format" value="best[protocol^=http]" />
<input type="hidden" name="format" value="best{$protocol}" />
<a class="downloadBtn"
href="{$video->url|escape}">Download</a><br/>
{/if}

View file

@ -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);
}
}