diff --git a/FAQ.md b/FAQ.md index c03462e..90e10c7 100644 --- a/FAQ.md +++ b/FAQ.md @@ -15,10 +15,10 @@ Here are the parameters that you can set: * `youtubedl`: path to your youtube-dl binary * `python`: path to your python binary * `params`: an array of parameters to pass to youtube-dl -* `curl_params`: an array of parameters to pass to curl * `convert`: true to enable audio conversion * `avconv`: path to your avconv or ffmpeg binary * `rtmpdump`: path to your rtmpdump binary +* `remux`: enable remux mode (experimental) See [`config.example.yml`](config.example.yml) for default values. @@ -31,10 +31,10 @@ convert: true avconv: path/to/avconv ``` -You will also need to install `avconv` and `curl` on your server: +You will also need to install `avconv` on your server: ```bash -sudo apt-get install libav-tools curl +sudo apt-get install libav-tools ``` ## How do I deploy Alltube on Heroku? @@ -129,3 +129,12 @@ And you probably need to run this in another terminal after `heroku local` has f ```bash chmod 0667 /tmp/heroku.fcgi.5000.sock ``` + +## How can I download 1080p videos from Youtube? + +Youtube distributes HD content in two separate video and audio files. +So Alltube will offer you video-only and audio-only formats in the format list. + +You then need to merge them together with a tool like ffmpeg. + +You can also enable the experimental remux mode that will merge the best video and the best audio format on the fly. diff --git a/README.md b/README.md index 5a3c08f..776aca9 100644 --- a/README.md +++ b/README.md @@ -122,13 +122,13 @@ server { ## Other dependencies -You need [avconv](https://libav.org/avconv.html), [rtmpdump](http://rtmpdump.mplayerhq.hu/) and [curl](https://curl.haxx.se/) in order to enable conversions. +You need [avconv](https://libav.org/avconv.html) and [rtmpdump](http://rtmpdump.mplayerhq.hu/) in order to enable conversions. If you don't want to enable conversions, you can disable it in `config.yml`. On Debian-based systems: ```bash -sudo apt-get install libav-tools rtmpdump curl +sudo apt-get install libav-tools rtmpdump ``` You also probably need to edit the `avconv` variable in `config.yml` so that it points to your ffmpeg/avconv binary (`/usr/bin/avconv` on Debian/Ubuntu). diff --git a/classes/Config.php b/classes/Config.php index 0c3fde3..be22a3e 100644 --- a/classes/Config.php +++ b/classes/Config.php @@ -38,7 +38,7 @@ class Config * * @var array */ - public $params = ['--no-playlist', '--no-warnings', '--playlist-end', 1]; + public $params = ['--no-warnings', '--ignore-errors', '--flat-playlist']; /** * Enable audio conversion. @@ -61,20 +61,6 @@ class Config */ public $rtmpdump = 'vendor/bin/rtmpdump'; - /** - * curl binary path. - * - * @var string - */ - public $curl = '/usr/bin/curl'; - - /** - * curl parameters. - * - * @var array - */ - public $curl_params = []; - /** * Disable URL rewriting. * @@ -89,6 +75,13 @@ class Config */ public $stream = false; + /** + * Allow to remux video + audio? + * + * @var bool + */ + public $remux = false; + /** * YAML config file path. * @@ -104,9 +97,7 @@ class Config * * python: Python binary path * * avconv: avconv or ffmpeg binary path * * rtmpdump: rtmpdump binary path - * * curl: curl binary path * * params: Array of youtube-dl parameters - * * curl_params: Array of curl parameters * * convert: Enable conversion? * * @param array $options Options @@ -141,7 +132,7 @@ class Config if (is_null(self::$instance) || self::$instance->file != $yamlfile) { if (is_file($yamlfile)) { $options = Yaml::parse(file_get_contents($yamlPath)); - } elseif ($yamlfile == 'config.yml') { + } elseif ($yamlfile == 'config.yml' || empty($yamlfile)) { /* Allow for the default file to be missing in order to not surprise users that did not create a config file diff --git a/classes/VideoDownload.php b/classes/VideoDownload.php index 345d2eb..0d42a97 100644 --- a/classes/VideoDownload.php +++ b/classes/VideoDownload.php @@ -98,7 +98,7 @@ class VideoDownload throw new \Exception($errorOutput); } } else { - return $process->getOutput(); + return trim($process->getOutput()); } } @@ -113,21 +113,25 @@ class VideoDownload * */ public function getJSON($url, $format = null, $password = null) { - return json_decode($this->getProp($url, $format, 'dump-json', $password)); + return json_decode($this->getProp($url, $format, 'dump-single-json', $password)); } /** * Get URL of video from URL of page. * + * It generally returns only one URL. + * But it can return two URLs when multiple formats are specified + * (eg. bestvideo+bestaudio). + * * @param string $url URL of page * @param string $format Format to use for the video * @param string $password Video password * - * @return string URL of video + * @return string[] URLs of video * */ public function getURL($url, $format = null, $password = null) { - return $this->getProp($url, $format, 'get-url', $password); + return explode(PHP_EOL, $this->getProp($url, $format, 'get-url', $password)); } /** @@ -144,6 +148,28 @@ class VideoDownload return trim($this->getProp($url, $format, 'get-filename', $password)); } + /** + * Get filename of video with the specified extension. + * + * @param string $extension New file extension + * @param string $url URL of page + * @param string $format Format to use for the video + * @param string $password Video password + * + * @return string Filename of extracted video with specified extension + */ + public function getFileNameWithExtension($extension, $url, $format = null, $password = null) + { + return html_entity_decode( + pathinfo( + $this->getFilename($url, $format, $password), + PATHINFO_FILENAME + ).'.'.$extension, + ENT_COMPAT, + 'ISO-8859-1' + ); + } + /** * Get filename of audio from URL of page. * @@ -155,14 +181,7 @@ class VideoDownload * */ public function getAudioFilename($url, $format = null, $password = null) { - return html_entity_decode( - pathinfo( - $this->getFilename($url, $format, $password), - PATHINFO_FILENAME - ).'.mp3', - ENT_COMPAT, - 'ISO-8859-1' - ); + return $this->getFileNameWithExtension('mp3', $url, $format, $password); } /** @@ -222,31 +241,30 @@ class VideoDownload } /** - * Get a process that runs curl in order to download a video. + * Get a process that runs avconv in order to convert a video to MP3. * - * @param object $video Video object returned by youtube-dl + * @param string $url URL of the video file * * @return \Symfony\Component\Process\Process Process */ - private function getCurlProcess($video) + private function getAvconvMp3Process($url) { - if (!shell_exec('which '.$this->config->curl)) { - throw(new \Exception('Can\'t find curl')); + if (!shell_exec('which '.$this->config->avconv)) { + throw(new \Exception('Can\'t find avconv or ffmpeg')); } - $builder = ProcessBuilder::create( - array_merge( - [ - $this->config->curl, - '--silent', - '--location', - '--user-agent', $video->http_headers->{'User-Agent'}, - $video->url, - ], - $this->config->curl_params - ) - ); - return $builder->getProcess(); + return ProcessBuilder::create( + [ + $this->config->avconv, + '-v', 'quiet', + //Vimeo needs a correct user-agent + '-user-agent', $this->getProp(null, null, 'dump-user-agent'), + '-i', $url, + '-f', 'mp3', + '-vn', + 'pipe:1', + ] + ); } /** @@ -260,40 +278,22 @@ class VideoDownload */ public function getAudioStream($url, $format, $password = null) { - if (!shell_exec('which '.$this->config->avconv)) { - throw(new \Exception('Can\'t find avconv or ffmpeg')); - } - $video = $this->getJSON($url, $format, $password); if (in_array($video->protocol, ['m3u8', 'm3u8_native'])) { throw(new \Exception('Conversion of M3U8 files is not supported.')); } - //Vimeo needs a correct user-agent - ini_set( - 'user_agent', - $video->http_headers->{'User-Agent'} - ); - $avconvProc = ProcessBuilder::create( - [ - $this->config->avconv, - '-v', 'quiet', - '-i', '-', - '-f', 'mp3', - '-vn', - 'pipe:1', - ] - ); - if (parse_url($video->url, PHP_URL_SCHEME) == 'rtmp') { $process = $this->getRtmpProcess($video); - } else { - $process = $this->getCurlProcess($video); - } - $chain = new Chain($process); - $chain->add('|', $avconvProc); + $chain = new Chain($process); + $chain->add('|', $this->getAvconvMp3Process('-')); - return popen($chain->getProcess()->getCommandLine(), 'r'); + return popen($chain->getProcess()->getCommandLine(), 'r'); + } else { + $avconvProc = $this->getAvconvMp3Process($video->url); + + return popen($avconvProc->getProcess()->getCommandLine(), 'r'); + } } /** @@ -324,4 +324,42 @@ class VideoDownload return popen($procBuilder->getProcess()->getCommandLine(), 'r'); } + + /** + * Get an avconv stream to remux audio and video. + * + * @param array $urls URLs of the video ($urls[0]) and audio ($urls[1]) files + * + * @return resource popen stream + */ + public function getRemuxStream(array $urls) + { + $procBuilder = ProcessBuilder::create( + [ + $this->config->avconv, + '-v', 'quiet', + '-i', $urls[0], + '-i', $urls[1], + '-c', 'copy', + '-map', '0:v:0 ', + '-map', '1:a:0', + '-f', 'matroska', + 'pipe:1', + ] + ); + + return popen($procBuilder->getProcess()->getCommandLine(), 'r'); + } + + /** + * Get video stream from an RTMP video. + * + * @param \stdClass $video Video object returned by getJSON + * + * @return resource popen stream + */ + public function getRtmpStream(\stdClass $video) + { + return popen($this->getRtmpProcess($video)->getCommandLine(), 'r'); + } } diff --git a/classes/ViewFactory.php b/classes/ViewFactory.php new file mode 100644 index 0000000..a046c3e --- /dev/null +++ b/classes/ViewFactory.php @@ -0,0 +1,42 @@ +getUri()); + $view->registerPlugin('function', 'path_for', [$smartyPlugins, 'pathFor']); + $view->registerPlugin('function', 'base_url', [$smartyPlugins, 'baseUrl']); + + $view->registerPlugin('modifier', 'noscheme', 'Smarty_Modifier_noscheme'); + + return $view; + } +} diff --git a/composer.json b/composer.json index 8c69933..870d923 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "type": "project", "require": { "smarty/smarty": "~3.1.29", - "slim/slim": "~3.7.0", + "slim/slim": "~3.8.1", "mathmarques/smarty-view": "~1.1.0", "symfony/yaml": "~3.2.0", "symfony/process": "~3.2.0", @@ -21,7 +21,7 @@ "squizlabs/php_codesniffer": "~2.8.0", "phpunit/phpunit": "~5.7.2", "ffmpeg/ffmpeg": "dev-release", - "rg3/youtube-dl": "~2017.04.15", + "rg3/youtube-dl": "~2017.04.28", "rudloff/rtmpdump-bin": "~2.3", "heroku/heroku-buildpack-php": "*" }, @@ -37,10 +37,10 @@ "type": "package", "package": { "name": "rg3/youtube-dl", - "version": "2017.04.15", + "version": "2017.04.28", "dist": { "type": "zip", - "url": "https://github.com/rg3/youtube-dl/archive/2017.04.15.zip" + "url": "https://github.com/rg3/youtube-dl/archive/2017.04.28.zip" } } }, diff --git a/composer.lock b/composer.lock index b9ce356..5d2f04b 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": "863f0c9fafcc85d185aa4441b0ad7e71", + "content-hash": "6fda75dec4d72b9620ca68697278dc7e", "packages": [ { "name": "aura/session", @@ -752,23 +752,24 @@ }, { "name": "slim/slim", - "version": "3.7.0", + "version": "3.8.1", "source": { "type": "git", "url": "https://github.com/slimphp/Slim.git", - "reference": "4254e40d81559e35cdf856bcbaca5f3af468b7ef" + "reference": "5385302707530b2bccee1769613ad769859b826d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slimphp/Slim/zipball/4254e40d81559e35cdf856bcbaca5f3af468b7ef", - "reference": "4254e40d81559e35cdf856bcbaca5f3af468b7ef", + "url": "https://api.github.com/repos/slimphp/Slim/zipball/5385302707530b2bccee1769613ad769859b826d", + "reference": "5385302707530b2bccee1769613ad769859b826d", "shasum": "" }, "require": { - "container-interop/container-interop": "^1.1", + "container-interop/container-interop": "^1.2", "nikic/fast-route": "^1.0", "php": ">=5.5.0", "pimple/pimple": "^3.0", + "psr/container": "^1.0", "psr/http-message": "^1.0" }, "provide": { @@ -818,7 +819,7 @@ "micro", "router" ], - "time": "2016-12-20T20:30:47+00:00" + "time": "2017-03-19T17:55:20+00:00" }, { "name": "smarty/smarty", @@ -1734,10 +1735,10 @@ }, { "name": "rg3/youtube-dl", - "version": "2017.04.15", + "version": "2017.04.28", "dist": { "type": "zip", - "url": "https://github.com/rg3/youtube-dl/archive/2017.04.15.zip", + "url": "https://github.com/rg3/youtube-dl/archive/2017.04.28.zip", "reference": null, "shasum": null }, diff --git a/config.example.yml b/config.example.yml index 6bb93e4..2bdfc78 100644 --- a/config.example.yml +++ b/config.example.yml @@ -5,10 +5,8 @@ params: - --no-warnings - --playlist-end - 1 -curl_params: convert: false 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 8c82779..16d22ee 100644 --- a/controllers/FrontController.php +++ b/controllers/FrontController.php @@ -8,7 +8,7 @@ namespace Alltube\Controller; use Alltube\Config; use Alltube\PasswordException; use Alltube\VideoDownload; -use Interop\Container\ContainerInterface; +use Psr\Container\ContainerInterface; use Slim\Container; use Slim\Http\Request; use Slim\Http\Response; @@ -65,15 +65,21 @@ class FrontController * FrontController constructor. * * @param Container $container Slim dependency container + * @param Config $config Config instance + * @param array $cookies Cookie array */ - public function __construct(ContainerInterface $container) + public function __construct(ContainerInterface $container, Config $config = null, array $cookies = []) { - $this->config = Config::getInstance(); + if (isset($config)) { + $this->config = $config; + } else { + $this->config = Config::getInstance(); + } $this->download = new VideoDownload(); $this->container = $container; $this->view = $this->container->get('view'); $session_factory = new \Aura\Session\SessionFactory(); - $session = $session_factory->newInstance($_COOKIE); + $session = $session_factory->newInstance($cookies); $this->sessionSegment = $session->getSegment('Alltube\Controller\FrontController'); if ($this->config->stream) { $this->defaultFormat = 'best'; @@ -173,9 +179,9 @@ class FrontController if ($this->config->stream) { return $this->getStream($params['url'], 'mp3', $response, $request, $password); } else { - $url = $this->download->getURL($params['url'], 'mp3[protocol^=http]', $password); + $urls = $this->download->getURL($params['url'], 'mp3[protocol^=http]', $password); - return $response->withRedirect($url); + return $response->withRedirect($urls[0]); } } catch (PasswordException $e) { return $this->password($request, $response); @@ -218,18 +224,31 @@ class FrontController } else { $protocol = '[protocol^=http]'; } + if (isset($video->entries)) { + $template = 'playlist.tpl'; + } else { + $template = 'video.tpl'; + } + if (isset($video->title)) { + $title = $video->title; + $description = 'Download "'.$video->title.'" from '.$video->extractor_key; + } else { + $title = 'Video download'; + $description = 'Download video from '.$video->extractor_key; + } $this->view->render( $response, - 'video.tpl', + $template, [ 'video' => $video, 'class' => 'video', - 'title' => $video->title, - 'description' => 'Download "'.$video->title.'" from '.$video->extractor_key, + 'title' => $title, + 'description' => $description, 'protocol' => $protocol, 'config' => $this->config, 'canonical' => $this->getCanonicalUrl($request), 'uglyUrls' => $this->config->uglyUrls, + 'remux' => $this->config->remux, ] ); @@ -298,10 +317,16 @@ class FrontController * * @return Response HTTP response */ - private function getStream($url, $format, $response, $request, $password = null) + private function getStream($url, $format, Response $response, Request $request, $password = null) { $video = $this->download->getJSON($url, $format, $password); - if ($video->protocol == 'm3u8') { + if ($video->protocol == 'rtmp') { + $stream = $this->download->getRtmpStream($video); + $response = $response->withHeader('Content-Type', 'video/'.$video->ext); + if ($request->isGet()) { + $response = $response->withBody(new Stream($stream)); + } + } elseif ($video->protocol == 'm3u8') { $stream = $this->download->getM3uStream($video); $response = $response->withHeader('Content-Type', 'video/'.$video->ext); if ($request->isGet()) { @@ -316,11 +341,98 @@ class FrontController $response = $response->withBody($stream->getBody()); } } - $response = $response->withHeader('Content-Disposition', 'attachment; filename="'.$video->_filename.'"'); + $response = $response->withHeader( + 'Content-Disposition', + 'attachment; filename="'. + $this->download->getFilename($url, $format, $password).'"' + ); return $response; } + /** + * Get a remuxed stream piped through the server. + * + * @param array $urls URLs of the video and audio files + * @param string $format Requested format + * @param Response $response PSR-7 response + * @param Request $request PSR-7 request + * + * @return Response HTTP response + */ + private function getRemuxStream(array $urls, $format, Response $response, Request $request) + { + if (!$this->config->remux) { + throw new \Exception('You need to enable remux mode to merge two formats.'); + } + $stream = $this->download->getRemuxStream($urls); + $response = $response->withHeader('Content-Type', 'video/x-matroska'); + if ($request->isGet()) { + $response = $response->withBody(new Stream($stream)); + } + $webpageUrl = $request->getQueryParam('url'); + + return $response->withHeader('Content-Disposition', 'attachment; filename="'.pathinfo( + $this->download->getFileNameWithExtension( + 'mkv', + $webpageUrl, + $format, + $this->sessionSegment->getFlash($webpageUrl) + ), + PATHINFO_FILENAME + ).'.mkv"'); + } + + /** + * Get video format from request parameters or default format if none is specified. + * + * @param Request $request PSR-7 request + * + * @return string format + */ + private function getFormat(Request $request) + { + $format = $request->getQueryParam('format'); + if (!isset($format)) { + $format = $this->defaultFormat; + } + + return $format; + } + + /** + * Get approriate HTTP response to redirect query + * Depends on whether we want to stream, remux or simply redirect. + * + * @param string $url URL of the video + * @param string $format Requested format + * @param Response $response PSR-7 response + * @param Request $request PSR-7 request + * + * @return Response HTTP response + */ + private function getRedirectResponse($url, $format, Response $response, Request $request) + { + $videoUrls = $this->download->getURL( + $url, + $format, + $this->sessionSegment->getFlash($url) + ); + if (count($videoUrls) > 1) { + return $this->getRemuxStream($videoUrls, $format, $response, $request); + } elseif ($this->config->stream) { + return $this->getStream( + $url, + $format, + $response, + $request, + $this->sessionSegment->getFlash($url) + ); + } else { + return $response->withRedirect($videoUrls[0]); + } + } + /** * Redirect to video file. * @@ -331,34 +443,14 @@ class FrontController */ public function redirect(Request $request, Response $response) { - $params = $request->getQueryParams(); - if (isset($params['format'])) { - $format = $params['format']; - } else { - $format = $this->defaultFormat; - } - if (isset($params['url'])) { + $url = $request->getQueryParam('url'); + $format = $this->getFormat($request); + if (isset($url)) { try { - if ($this->config->stream) { - return $this->getStream( - $params['url'], - $format, - $response, - $request, - $this->sessionSegment->getFlash($params['url']) - ); - } else { - $url = $this->download->getURL( - $params['url'], - $format, - $this->sessionSegment->getFlash($params['url']) - ); - - return $response->withRedirect($url); - } + return $this->getRedirectResponse($url, $format, $response, $request); } catch (PasswordException $e) { return $response->withRedirect( - $this->container->get('router')->pathFor('video').'?url='.urlencode($params['url']) + $this->container->get('router')->pathFor('video').'?url='.urlencode($url) ); } catch (\Exception $e) { $response->getBody()->write($e->getMessage()); diff --git a/css/style.css b/css/style.css index 31ae741..a055a97 100644 --- a/css/style.css +++ b/css/style.css @@ -397,10 +397,35 @@ padding:3px; +/* Playlists */ +.playlist-entry .thumb { + float: left; + margin-right: 1em; +} +.playlist-entry { + clear: both; + padding-top: 2em; + text-align: left; + width: 600px; +} +.playlist-entry h3 { + margin-top: 0; +} +.playlist-entry h3 a { + text-decoration: none; +} +.playlist-entry h3 a:hover { + text-decoration: underline; +} + +.playlist-entry .downloadBtn { + font-size: 16px; + border-width: 2px; +} @@ -658,6 +683,16 @@ h1 { text-align:left; } + .playlist-entry { + text-align: center; + width: auto; + } + + .playlist-entry .thumb { + float: none; + margin-right: 0; + } + } @media all and (display-mode: standalone) { diff --git a/index.php b/index.php index b3b77de..0bb2c6e 100644 --- a/index.php +++ b/index.php @@ -4,31 +4,23 @@ require_once __DIR__.'/vendor/autoload.php'; use Alltube\Config; use Alltube\Controller\FrontController; use Alltube\UglyRouter; +use Alltube\ViewFactory; +use Slim\App; if (isset($_SERVER['REQUEST_URI']) && strpos($_SERVER['REQUEST_URI'], '/index.php') !== false) { header('Location: '.str_ireplace('/index.php', '/', $_SERVER['REQUEST_URI'])); die; } -$app = new \Slim\App(); +$app = new App(); $container = $app->getContainer(); $config = Config::getInstance(); if ($config->uglyUrls) { $container['router'] = new UglyRouter(); } -$container['view'] = function ($c) { - $view = new \Slim\Views\Smarty(__DIR__.'/templates/'); +$container['view'] = ViewFactory::create($container); - $smartyPlugins = new \Slim\Views\SmartyPlugins($c['router'], $c['request']->getUri()); - $view->registerPlugin('function', 'path_for', [$smartyPlugins, 'pathFor']); - $view->registerPlugin('function', 'base_url', [$smartyPlugins, 'baseUrl']); - - $view->registerPlugin('modifier', 'noscheme', 'Smarty_Modifier_noscheme'); - - return $view; -}; - -$controller = new FrontController($container); +$controller = new FrontController($container, null, $_COOKIE); $container['errorHandler'] = [$controller, 'error']; diff --git a/manifest.webapp b/manifest.webapp deleted file mode 100644 index 91435b8..0000000 --- a/manifest.webapp +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "AllTube", - "description": "Easily download videos from Youtube, Dailymotion, Vimeo and other websites", - "developer": { - "name": "Pierre Rudloff", - "url": "https://rudloff.pro/" - }, - "icons": { - "32": "/img/favicon.png", - "60": "/img/logo_60.png", - "90": "/img/logo_90.png", - "243": "/img/logo_app.png", - "250": "/img/logo_250.png" - }, - "default_locale": "en", - "launch_path": "/index.php" -} diff --git a/package.json b/package.json index 9469e74..2c3b8e0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "alltube", "description": "HTML GUI for youtube-dl", - "version": "0.8.1-beta", + "version": "0.9.0", "author": "Pierre Rudloff", "bugs": "https://github.com/Rudloff/alltube/issues", "dependencies": { diff --git a/templates/playlist.tpl b/templates/playlist.tpl new file mode 100644 index 0000000..f6c9f22 --- /dev/null +++ b/templates/playlist.tpl @@ -0,0 +1,29 @@ +{include file="inc/head.tpl"} +
+
+{include file="inc/logo.tpl"} +

Videos extracted from the {if isset($video->title)} + +{$video->title}{/if} playlist: +

+{foreach $video->entries as $video} +
+

+ {if !isset($video->title) and $video->ie_key == YoutubePlaylist} + Playlist + {else} + {$video->title} + {/if} +

+ url}">Download + url}">More options +
+{/foreach} + +
+{include file="inc/footer.tpl"} diff --git a/templates/video.tpl b/templates/video.tpl index 8872b1e..64ca555 100644 --- a/templates/video.tpl +++ b/templates/video.tpl @@ -34,6 +34,11 @@ Best ({$video->ext}) {/strip} + {if $remux} + + {/if} diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php index 0574eaf..f0ba659 100644 --- a/tests/ConfigTest.php +++ b/tests/ConfigTest.php @@ -45,7 +45,6 @@ class ConfigTest extends \PHPUnit_Framework_TestCase public function testGetInstance() { $this->assertEquals($this->config->convert, false); - $this->assertInternalType('array', $this->config->curl_params); $this->assertInternalType('array', $this->config->params); $this->assertInternalType('string', $this->config->youtubedl); $this->assertInternalType('string', $this->config->python); @@ -64,6 +63,16 @@ class ConfigTest extends \PHPUnit_Framework_TestCase Config::getInstance('foo'); } + /** + * Test the getInstance function with aen empty filename. + * + * @return void + */ + public function testGetInstanceWithEmptyFile() + { + Config::getInstance(''); + } + /** * Test the getInstance function with the CONVERT and PYTHON environment variables. * diff --git a/tests/FrontControllerTest.php b/tests/FrontControllerTest.php index 1d51e3d..859f7de 100644 --- a/tests/FrontControllerTest.php +++ b/tests/FrontControllerTest.php @@ -7,6 +7,7 @@ namespace Alltube\Test; use Alltube\Config; use Alltube\Controller\FrontController; +use Alltube\ViewFactory; use Slim\Container; use Slim\Http\Environment; use Slim\Http\Request; @@ -53,18 +54,8 @@ class FrontControllerTest extends \PHPUnit_Framework_TestCase $this->container = new Container(); $this->request = Request::createFromEnvironment(Environment::mock()); $this->response = new Response(); - $this->container['view'] = function ($c) { - $view = new \Slim\Views\Smarty(__DIR__.'/../templates/'); - - $smartyPlugins = new \Slim\Views\SmartyPlugins($c['router'], $this->request->getUri()); - $view->registerPlugin('function', 'path_for', [$smartyPlugins, 'pathFor']); - $view->registerPlugin('function', 'base_url', [$smartyPlugins, 'baseUrl']); - - $view->registerPlugin('modifier', 'noscheme', 'Smarty_Modifier_noscheme'); - - return $view; - }; - $this->controller = new FrontController($this->container); + $this->container['view'] = ViewFactory::create($this->container, $this->request); + $this->controller = new FrontController($this->container, Config::getInstance('config_test.yml')); $this->container['router']->map(['GET'], '/', [$this->controller, 'index']) ->setName('index'); $this->container['router']->map(['GET'], '/video', [$this->controller, 'video']) @@ -83,6 +74,82 @@ class FrontControllerTest extends \PHPUnit_Framework_TestCase Config::destroyInstance(); } + /** + * Run controller function with custom query parameters and return the result. + * + * @param string $request Controller function to call + * @param array $params Query parameters + * @param Config $config Custom config + * + * @return Response HTTP response + */ + private function getRequestResult($request, array $params, Config $config = null) + { + if (isset($config)) { + $controller = new FrontController($this->container, $config); + } else { + $controller = $this->controller; + } + + return $controller->$request( + $this->request->withQueryParams($params), + $this->response + ); + } + + /** + * Assert that calling controller function with these parameters returns a 200 HTTP response. + * + * @param string $request Controller function to call + * @param array $params Query parameters + * @param Config $config Custom config + * + * @return void + */ + private function assertRequestIsOk($request, array $params = [], Config $config = null) + { + $this->assertTrue($this->getRequestResult($request, $params, $config)->isOk()); + } + + /** + * Assert that calling controller function with these parameters returns an HTTP redirect. + * + * @param string $request Controller function to call + * @param array $params Query parameters + * @param Config $config Custom config + * + * @return void + */ + private function assertRequestIsRedirect($request, array $params = [], Config $config = null) + { + $this->assertTrue($this->getRequestResult($request, $params, $config)->isRedirect()); + } + + /** + * Assert that calling controller function with these parameters returns an HTTP redirect. + * + * @param string $request Controller function to call + * @param array $params Query parameters + * @param Config $config Custom config + * + * @return void + */ + private function assertRequestIsServerError($request, array $params = [], Config $config = null) + { + $this->assertTrue($this->getRequestResult($request, $params, $config)->isServerError()); + } + + /** + * Test the constructor. + * + * @return void + */ + public function testConstructor() + { + $controller = new FrontController($this->container); + $this->assertInstanceOf(FrontController::class, $controller); + } + /** * Test the constructor with streams enabled. * @@ -90,9 +157,7 @@ class FrontControllerTest extends \PHPUnit_Framework_TestCase */ public function testConstructorWithStream() { - $config = Config::getInstance(); - $config->stream = true; - $controller = new FrontController($this->container); + $controller = new FrontController($this->container, new Config(['stream'=>true])); $this->assertInstanceOf(FrontController::class, $controller); } @@ -103,8 +168,7 @@ class FrontControllerTest extends \PHPUnit_Framework_TestCase */ public function testIndex() { - $result = $this->controller->index($this->request, $this->response); - $this->assertTrue($result->isOk()); + $this->assertRequestIsOk('index'); } /** @@ -130,8 +194,7 @@ class FrontControllerTest extends \PHPUnit_Framework_TestCase */ public function testExtractors() { - $result = $this->controller->extractors($this->request, $this->response); - $this->assertTrue($result->isOk()); + $this->assertRequestIsOk('extractors'); } /** @@ -141,8 +204,7 @@ class FrontControllerTest extends \PHPUnit_Framework_TestCase */ public function testPassword() { - $result = $this->controller->password($this->request, $this->response); - $this->assertTrue($result->isOk()); + $this->assertRequestIsOk('password'); } /** @@ -152,8 +214,7 @@ class FrontControllerTest extends \PHPUnit_Framework_TestCase */ public function testVideoWithoutUrl() { - $result = $this->controller->video($this->request, $this->response); - $this->assertTrue($result->isRedirect()); + $this->assertRequestIsRedirect('video'); } /** @@ -163,11 +224,17 @@ class FrontControllerTest extends \PHPUnit_Framework_TestCase */ public function testVideo() { - $result = $this->controller->video( - $this->request->withQueryParams(['url'=>'https://www.youtube.com/watch?v=M7IpKCZ47pU']), - $this->response - ); - $this->assertTrue($result->isOk()); + $this->assertRequestIsOk('video', ['url'=>'https://www.youtube.com/watch?v=M7IpKCZ47pU']); + } + + /** + * Test the video() function with a video that does not have a title. + * + * @return void + */ + public function testVideoWithoutTitle() + { + $this->assertRequestIsOk('video', ['url'=>'http://html5demos.com/video']); } /** @@ -177,11 +244,7 @@ class FrontControllerTest extends \PHPUnit_Framework_TestCase */ public function testVideoWithAudio() { - $result = $this->controller->video( - $this->request->withQueryParams(['url'=>'https://www.youtube.com/watch?v=M7IpKCZ47pU', 'audio'=>true]), - $this->response - ); - $this->assertTrue($result->isOk()); + $this->assertRequestIsOk('video', ['url'=>'https://www.youtube.com/watch?v=M7IpKCZ47pU', 'audio'=>true]); } /** @@ -191,14 +254,10 @@ class FrontControllerTest extends \PHPUnit_Framework_TestCase */ public function testVideoWithUnconvertedAudio() { - $result = $this->controller->video( - $this->request->withQueryParams( - ['url' => 'https://2080.bandcamp.com/track/cygnus-x-the-orange-theme-2080-faulty-chip-cover', - 'audio'=> true, ] - ), - $this->response + $this->assertRequestIsRedirect( + 'video', + ['url'=> 'https://2080.bandcamp.com/track/cygnus-x-the-orange-theme-2080-faulty-chip-cover', 'audio'=>true] ); - $this->assertTrue($result->isRedirect()); } /** @@ -223,16 +282,8 @@ class FrontControllerTest extends \PHPUnit_Framework_TestCase */ public function testVideoWithMissingPassword() { - $result = $this->controller->video( - $this->request->withQueryParams(['url'=>'http://vimeo.com/68375962']), - $this->response - ); - $this->assertTrue($result->isOk()); - $result = $this->controller->video( - $this->request->withQueryParams(['url'=>'http://vimeo.com/68375962', 'audio'=>true]), - $this->response - ); - $this->assertTrue($result->isOk()); + $this->assertRequestIsOk('video', ['url'=>'http://vimeo.com/68375962']); + $this->assertRequestIsOk('video', ['url'=>'http://vimeo.com/68375962', 'audio'=>true]); } /** @@ -242,18 +293,26 @@ class FrontControllerTest extends \PHPUnit_Framework_TestCase */ public function testVideoWithStream() { - $config = Config::getInstance(); - $config->stream = true; - $result = $this->controller->video( - $this->request->withQueryParams(['url'=>'https://www.youtube.com/watch?v=M7IpKCZ47pU']), - $this->response + $config = new Config(['stream'=>true]); + $this->assertRequestIsOk('video', ['url'=>'https://www.youtube.com/watch?v=M7IpKCZ47pU'], $config); + $this->assertRequestIsOk( + 'video', + ['url'=> 'https://www.youtube.com/watch?v=M7IpKCZ47pU', 'audio'=>true], + $config ); - $this->assertTrue($result->isOk()); - $result = $this->controller->video( - $this->request->withQueryParams(['url'=>'https://www.youtube.com/watch?v=M7IpKCZ47pU', 'audio'=>true]), - $this->response + } + + /** + * Test the video() function with a playlist. + * + * @return void + */ + public function testVideoWithPlaylist() + { + $this->assertRequestIsOk( + 'video', + ['url'=> 'https://www.youtube.com/playlist?list=PLgdySZU6KUXL_8Jq5aUkyNV7wCa-4wZsC'] ); - $this->assertTrue($result->isOk()); } /** @@ -274,8 +333,7 @@ class FrontControllerTest extends \PHPUnit_Framework_TestCase */ public function testRedirectWithoutUrl() { - $result = $this->controller->redirect($this->request, $this->response); - $this->assertTrue($result->isRedirect()); + $this->assertRequestIsRedirect('redirect'); } /** @@ -285,11 +343,7 @@ class FrontControllerTest extends \PHPUnit_Framework_TestCase */ public function testRedirect() { - $result = $this->controller->redirect( - $this->request->withQueryParams(['url'=>'https://www.youtube.com/watch?v=M7IpKCZ47pU']), - $this->response - ); - $this->assertTrue($result->isRedirect()); + $this->assertRequestIsRedirect('redirect', ['url'=>'https://www.youtube.com/watch?v=M7IpKCZ47pU']); } /** @@ -299,11 +353,10 @@ class FrontControllerTest extends \PHPUnit_Framework_TestCase */ public function testRedirectWithFormat() { - $result = $this->controller->redirect( - $this->request->withQueryParams(['url'=>'https://www.youtube.com/watch?v=M7IpKCZ47pU', 'format'=>'worst']), - $this->response + $this->assertRequestIsRedirect( + 'redirect', + ['url'=> 'https://www.youtube.com/watch?v=M7IpKCZ47pU', 'format'=>'worst'] ); - $this->assertTrue($result->isRedirect()); } /** @@ -313,13 +366,11 @@ class FrontControllerTest extends \PHPUnit_Framework_TestCase */ public function testRedirectWithStream() { - $config = Config::getInstance(); - $config->stream = true; - $result = $this->controller->redirect( - $this->request->withQueryParams(['url'=>'https://www.youtube.com/watch?v=M7IpKCZ47pU']), - $this->response + $this->assertRequestIsOk( + 'redirect', + ['url'=> 'https://www.youtube.com/watch?v=M7IpKCZ47pU'], + new Config(['stream'=>true]) ); - $this->assertTrue($result->isOk()); } /** @@ -329,15 +380,58 @@ class FrontControllerTest extends \PHPUnit_Framework_TestCase */ public function testRedirectWithM3uStream() { - $config = Config::getInstance(); - $config->stream = true; - //We need to create a new controller instance in order to apply the custom config - $controller = new FrontController($this->container); - $result = $controller->redirect( - $this->request->withQueryParams(['url'=>'https://twitter.com/verge/status/813055465324056576/video/1']), - $this->response + $this->assertRequestIsOk( + 'redirect', + ['url'=> 'https://twitter.com/verge/status/813055465324056576/video/1'], + new Config(['stream'=>true]) + ); + } + + /** + * Test the redirect() function with an RTMP stream. + * + * @return void + */ + public function testRedirectWithRtmpStream() + { + $this->assertRequestIsOk( + 'redirect', + ['url'=> 'http://www.rtl2.de/sendung/grip-das-motormagazin/folge/folge-203-0'], + new Config(['stream'=>true]) + ); + } + + /** + * Test the redirect() function with a remuxed video. + * + * @return void + */ + public function testRedirectWithRemux() + { + $this->assertRequestIsOk( + 'redirect', + [ + 'url' => 'https://www.youtube.com/watch?v=M7IpKCZ47pU', + 'format'=> 'bestvideo+bestaudio', + ], + new Config(['remux'=>true]) + ); + } + + /** + * Test the redirect() function with a remuxed video but remux disabled. + * + * @return void + */ + public function testRedirectWithRemuxDisabled() + { + $this->assertRequestIsServerError( + 'redirect', + [ + 'url' => 'https://www.youtube.com/watch?v=M7IpKCZ47pU', + 'format'=> 'bestvideo+bestaudio', + ] ); - $this->assertTrue($result->isOk()); } /** @@ -347,11 +441,7 @@ class FrontControllerTest extends \PHPUnit_Framework_TestCase */ public function testRedirectWithMissingPassword() { - $result = $this->controller->redirect( - $this->request->withQueryParams(['url'=>'http://vimeo.com/68375962']), - $this->response - ); - $this->assertTrue($result->isRedirect()); + $this->assertRequestIsRedirect('redirect', ['url'=>'http://vimeo.com/68375962']); } /** @@ -361,10 +451,6 @@ class FrontControllerTest extends \PHPUnit_Framework_TestCase */ public function testRedirectWithError() { - $result = $this->controller->redirect( - $this->request->withQueryParams(['url'=>'http://example.com/foo']), - $this->response - ); - $this->assertTrue($result->isServerError()); + $this->assertRequestIsServerError('redirect', ['url'=>'http://example.com/foo']); } } diff --git a/tests/VideoDownloadTest.php b/tests/VideoDownloadTest.php index 39119db..aab0205 100644 --- a/tests/VideoDownloadTest.php +++ b/tests/VideoDownloadTest.php @@ -25,7 +25,7 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase */ protected function setUp() { - $this->download = new VideoDownload(); + $this->download = new VideoDownload(Config::getInstance('config_test.yml')); } /** @@ -84,11 +84,14 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase * * @return void * @dataProvider urlProvider + * @dataProvider m3uUrlProvider + * @dataProvider rtmpUrlProvider + * @dataProvider remuxUrlProvider */ public function testGetURL($url, $format, $filename, $extension, $domain) { $videoURL = $this->download->getURL($url, $format); - $this->assertContains($domain, $videoURL); + $this->assertContains($domain, $videoURL[0]); } /** @@ -98,7 +101,8 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase */ public function testGetURLWithPassword() { - $this->assertContains('vimeocdn.com', $this->download->getURL('http://vimeo.com/68375962', null, 'youtube-dl')); + $videoURL = $this->download->getURL('http://vimeo.com/68375962', null, 'youtube-dl'); + $this->assertContains('vimeocdn.com', $videoURL[0]); } /** @@ -184,6 +188,23 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase * * @return array[] */ + public function remuxUrlProvider() + { + return [ + [ + 'https://www.youtube.com/watch?v=M7IpKCZ47pU', 'bestvideo+bestaudio', + "It's Not Me, It's You - Hearts Under Fire-M7IpKCZ47pU", + 'mp4', + 'googlevideo.com', + ], + ]; + } + + /** + * Provides URLs for remux tests. + * + * @return array[] + */ public function m3uUrlProvider() { return [ @@ -196,6 +217,23 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase ]; } + /** + * Provides RTMP URLs for tests. + * + * @return array[] + */ + public function rtmpUrlProvider() + { + return [ + [ + 'http://www.rtl2.de/sendung/grip-das-motormagazin/folge/folge-203-0', 'bestaudio/best', + 'GRIP sucht den Sommerkönig-folge-203-0', + 'f4v', + 'edgefcs.net', + ], + ]; + } + /** * Provides incorrect URLs for tests. * @@ -215,8 +253,9 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase * @param string $format Format * * @return void - * @dataProvider URLProvider + * @dataProvider urlProvider * @dataProvider m3uUrlProvider + * @dataProvider rtmpUrlProvider */ public function testGetJSON($url, $format) { @@ -227,7 +266,6 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase $this->assertObjectHasAttribute('title', $info); $this->assertObjectHasAttribute('extractor_key', $info); $this->assertObjectHasAttribute('formats', $info); - $this->assertObjectHasAttribute('_filename', $info); } /** @@ -255,6 +293,8 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase * @return void * @dataProvider urlProvider * @dataProvider m3uUrlProvider + * @dataProvider rtmpUrlProvider + * @dataProvider remuxUrlProvider */ public function testGetFilename($url, $format, $filename, $extension) { @@ -286,6 +326,8 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase * @return void * @dataProvider urlProvider * @dataProvider m3uUrlProvider + * @dataProvider rtmpUrlProvider + * @dataProvider remuxUrlProvider */ public function testGetAudioFilename($url, $format, $filename) { @@ -321,9 +363,8 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase */ public function testGetAudioStreamAvconvError($url, $format) { - $config = Config::getInstance(); - $config->avconv = 'foobar'; - $this->download->getAudioStream($url, $format); + $download = new VideoDownload(new Config(['avconv'=>'foobar'])); + $download->getAudioStream($url, $format); } /** @@ -334,14 +375,12 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase * * @return void * @expectedException Exception - * @dataProvider urlProvider + * @dataProvider rtmpUrlProvider */ - public function testGetAudioStreamCurlError($url, $format) + public function testGetAudioStreamRtmpError($url, $format) { - $config = Config::getInstance(); - $config->curl = 'foobar'; - $config->rtmpdump = 'foobar'; - $this->download->getAudioStream($url, $format); + $download = new VideoDownload(new Config(['rtmpdump'=>'foobar'])); + $download->getAudioStream($url, $format); } /** @@ -359,6 +398,19 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase $this->download->getAudioStream($url, $format); } + /** + * Assert that a stream is valid. + * + * @param resource $stream Stream + * + * @return void + */ + private function assertStream($stream) + { + $this->assertInternalType('resource', $stream); + $this->assertFalse(feof($stream)); + } + /** * Test getM3uStream function. * @@ -370,10 +422,46 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase */ public function testGetM3uStream($url, $format) { - $video = $this->download->getJSON($url, $format); - $stream = $this->download->getM3uStream($video); - $this->assertInternalType('resource', $stream); - $this->assertFalse(feof($stream)); + $this->assertStream( + $this->download->getM3uStream( + $this->download->getJSON($url, $format) + ) + ); + } + + /** + * Test getRemuxStream function. + * + * @param string $url URL + * @param string $format Format + * + * @return void + * @dataProvider remuxUrlProvider + */ + public function testGetRemuxStream($url, $format) + { + $urls = $this->download->getURL($url, $format); + if (count($urls) > 1) { + $this->assertStream($this->download->getRemuxStream($urls)); + } + } + + /** + * Test getRtmpStream function. + * + * @param string $url URL + * @param string $format Format + * + * @return void + * @dataProvider rtmpUrlProvider + */ + public function testGetRtmpStream($url, $format) + { + $this->assertStream( + $this->download->getRtmpStream( + $this->download->getJSON($url, $format) + ) + ); } /** @@ -388,9 +476,8 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase */ public function testGetM3uStreamAvconvError($url, $format) { - $config = \Alltube\Config::getInstance(); - $config->avconv = 'foobar'; - $video = $this->download->getJSON($url, $format); - $this->download->getM3uStream($video); + $download = new VideoDownload(new Config(['avconv'=>'foobar'])); + $video = $download->getJSON($url, $format); + $download->getM3uStream($video); } } diff --git a/tests/ViewFactoryTest.php b/tests/ViewFactoryTest.php new file mode 100644 index 0000000..9155faa --- /dev/null +++ b/tests/ViewFactoryTest.php @@ -0,0 +1,27 @@ +assertInstanceOf(Smarty::class, $view); + } +}