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 * @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. * Enable audio conversion.
@ -82,6 +82,12 @@ class Config
*/ */
public $uglyUrls = false; public $uglyUrls = false;
/**
* Stream downloaded files trough server?
* @var boolean
*/
public $stream = false;
/** /**
* YAML config file path. * YAML config file path.
* *

View file

@ -295,4 +295,33 @@ class VideoDownload
return popen($chain->getProcess()->getCommandLine(), 'r'); 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", "symfony/process": "~3.2.0",
"ptachoire/process-builder-chain": "~1.2.0", "ptachoire/process-builder-chain": "~1.2.0",
"rudloff/smarty-plugin-noscheme": "~0.1.0", "rudloff/smarty-plugin-noscheme": "~0.1.0",
"guzzlehttp/guzzle": "~6.2.0",
"rudloff/rtmpdump-bin": "~2.3",
"aura/session": "~2.1.0" "aura/session": "~2.1.0"
}, },
"require-dev": { "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", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "15507d8a1cb225e2e118500a7883b255", "content-hash": "44b24b403652e1a7e450280e4643e37e",
"packages": [ "packages": [
{ {
"name": "aura/session", "name": "aura/session",
@ -95,6 +95,177 @@
"description": "Promoting the interoperability of container objects (DIC, SL, etc.)", "description": "Promoting the interoperability of container objects (DIC, SL, etc.)",
"time": "2014-12-30T15:22:37+00:00" "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", "name": "jeremykendall/php-domain-parser",
"version": "3.0.0", "version": "3.0.0",
@ -446,6 +617,34 @@
"description": "Add ability to chain symfony processes", "description": "Add ability to chain symfony processes",
"time": "2016-04-10T08:33:20+00:00" "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", "name": "rudloff/smarty-plugin-noscheme",
"version": "0.1.1", "version": "0.1.1",
@ -1475,34 +1674,6 @@
}, },
"type": "library" "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", "name": "sebastian/code-unit-reverse-lookup",
"version": "1.0.0", "version": "1.0.0",

View file

@ -3,7 +3,6 @@ python: /usr/bin/python
params: params:
- --no-playlist - --no-playlist
- --no-warnings - --no-warnings
- -f best[protocol^=http]
- --playlist-end - --playlist-end
- 1 - 1
curl_params: curl_params:
@ -12,3 +11,4 @@ avconv: vendor/bin/ffmpeg
rtmpdump: vendor/bin/rtmpdump rtmpdump: vendor/bin/rtmpdump
curl: /usr/bin/curl curl: /usr/bin/curl
uglyUrls: false uglyUrls: false
stream: false

View file

@ -68,6 +68,11 @@ class FrontController
$session_factory = new \Aura\Session\SessionFactory(); $session_factory = new \Aura\Session\SessionFactory();
$session = $session_factory->newInstance($_COOKIE); $session = $session_factory->newInstance($_COOKIE);
$this->sessionSegment = $session->getSegment('Alltube\Controller\FrontController'); $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'])) { if (isset($params['audio'])) {
try { 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) { } catch (PasswordException $e) {
return $this->password($request, $response); return $this->password($request, $response);
} catch (\Exception $e) { } catch (\Exception $e) {
@ -178,10 +187,15 @@ class FrontController
} }
} else { } else {
try { try {
$video = $this->download->getJSON($params['url'], null, $password); $video = $this->download->getJSON($params['url'], $this->defaultFormat, $password);
} catch (PasswordException $e) { } catch (PasswordException $e) {
return $this->password($request, $response); return $this->password($request, $response);
} }
if ($this->config->stream) {
$protocol = '';
} else {
$protocol = '[protocol^=http]';
}
$this->view->render( $this->view->render(
$response, $response,
'video.tpl', 'video.tpl',
@ -190,6 +204,8 @@ class FrontController
'class' => 'video', 'class' => 'video',
'title' => $video->title, 'title' => $video->title,
'description' => 'Download "'.$video->title.'" from '.$video->extractor_key, 'description' => 'Download "'.$video->title.'" from '.$video->extractor_key,
'protocol' => $protocol,
'config' => $this->config,
] ]
); );
} }
@ -222,6 +238,43 @@ class FrontController
return $response->withStatus(500); 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. * Redirect to video file.
* *
@ -235,13 +288,23 @@ class FrontController
$params = $request->getQueryParams(); $params = $request->getQueryParams();
if (isset($params['url'])) { if (isset($params['url'])) {
try { try {
$url = $this->download->getURL( if ($this->config->stream) {
$params['url'], return $this->getStream(
$request->getParam('format'), $params['url'],
$this->sessionSegment->getFlash($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) { } catch (PasswordException $e) {
return $response->withRedirect( return $response->withRedirect(
$this->container->get('router')->pathFor('video').'?url='.urlencode($params['url']) $this->container->get('router')->pathFor('video').'?url='.urlencode($params['url'])

View file

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

View file

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

View file

@ -146,7 +146,7 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase
{ {
return [ 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", "It's Not Me, It's You - Hearts Under Fire-M7IpKCZ47pU",
'mp4', 'mp4',
'googlevideo.com', 'googlevideo.com',
@ -159,7 +159,7 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase
'googlevideo.com', 'googlevideo.com',
], ],
[ [
'https://vimeo.com/24195442', null, 'https://vimeo.com/24195442', 'best[protocol^=http]',
'Carving the Mountains-24195442', 'Carving the Mountains-24195442',
'mp4', 'mp4',
'vimeocdn.com', '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. * Provides incorrect URLs for tests.
* *
@ -199,6 +216,7 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase
* *
* @return void * @return void
* @dataProvider URLProvider * @dataProvider URLProvider
* @dataProvider M3uUrlProvider
*/ */
public function testGetJSON($url, $format) public function testGetJSON($url, $format)
{ {
@ -207,6 +225,7 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase
$this->assertObjectHasAttribute('url', $info); $this->assertObjectHasAttribute('url', $info);
$this->assertObjectHasAttribute('ext', $info); $this->assertObjectHasAttribute('ext', $info);
$this->assertObjectHasAttribute('title', $info); $this->assertObjectHasAttribute('title', $info);
$this->assertObjectHasAttribute('extractor_key', $info);
$this->assertObjectHasAttribute('formats', $info); $this->assertObjectHasAttribute('formats', $info);
$this->assertObjectHasAttribute('_filename', $info); $this->assertObjectHasAttribute('_filename', $info);
} }
@ -235,6 +254,7 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase
* *
* @return void * @return void
* @dataProvider urlProvider * @dataProvider urlProvider
* @dataProvider M3uUrlProvider
*/ */
public function testGetFilename($url, $format, $filename, $extension) public function testGetFilename($url, $format, $filename, $extension)
{ {
@ -267,6 +287,7 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase
* *
* @return void * @return void
* @dataProvider urlProvider * @dataProvider urlProvider
* @dataProvider M3uUrlProvider
*/ */
public function testGetAudioFilename($url, $format, $filename) public function testGetAudioFilename($url, $format, $filename)
{ {
@ -302,7 +323,7 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase
*/ */
public function testGetAudioStreamAvconvError($url, $format) public function testGetAudioStreamAvconvError($url, $format)
{ {
$config = \Alltube\Config::getInstance(); $config = Config::getInstance();
$config->avconv = 'foobar'; $config->avconv = 'foobar';
$this->download->getAudioStream($url, $format); $this->download->getAudioStream($url, $format);
} }
@ -319,7 +340,7 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase
*/ */
public function testGetAudioStreamCurlError($url, $format) public function testGetAudioStreamCurlError($url, $format)
{ {
$config = \Alltube\Config::getInstance(); $config = Config::getInstance();
$config->curl = 'foobar'; $config->curl = 'foobar';
$config->rtmpdump = 'foobar'; $config->rtmpdump = 'foobar';
$this->download->getAudioStream($url, $format); $this->download->getAudioStream($url, $format);
@ -328,11 +349,50 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase
/** /**
* Test getAudioStream function with a M3U8 file. * Test getAudioStream function with a M3U8 file.
* *
* @param string $url URL
* @param string $format Format
*
* @return void * @return void
* @expectedException Exception * @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);
} }
} }