From 4c9af8ad1d89f89d2ea9390247639793fb4b3d90 Mon Sep 17 00:00:00 2001 From: Pierre Rudloff Date: Sun, 21 Apr 2019 18:30:02 +0200 Subject: [PATCH] refactor: New Video class The news class provides a cleaner object-oriented logic BREAKING CHANGE: The VideoDownload class has been removed and the Config constructor is now private --- README.md | 16 +- classes/Config.php | 94 ++++-- classes/PlaylistArchiveStream.php | 151 +++++----- classes/PlaylistArchiveVideo.php | 43 --- classes/{VideoDownload.php => Video.php} | 277 ++++++++++-------- controllers/FrontController.php | 224 ++++++-------- index.php | 6 +- tests/BaseTest.php | 42 +++ tests/ConfigTest.php | 85 ++++-- tests/FrontControllerTest.php | 116 +++----- tests/LocaleManagerTest.php | 2 +- tests/LocaleMiddlewareTest.php | 2 +- tests/LocaleTest.php | 2 +- tests/PlaylistArchiveStreamTest.php | 43 +-- tests/UglyRouterTest.php | 2 +- ...wnloadStubsTest.php => VideoStubsTest.php} | 48 +-- .../{VideoDownloadTest.php => VideoTest.php} | 229 ++++++--------- tests/ViewFactoryTest.php | 2 +- 18 files changed, 665 insertions(+), 719 deletions(-) delete mode 100644 classes/PlaylistArchiveVideo.php rename classes/{VideoDownload.php => Video.php} (64%) create mode 100644 tests/BaseTest.php rename tests/{VideoDownloadStubsTest.php => VideoStubsTest.php} (59%) rename tests/{VideoDownloadTest.php => VideoTest.php} (68%) diff --git a/README.md b/README.md index db3d170..9c99f51 100644 --- a/README.md +++ b/README.md @@ -162,19 +162,17 @@ You can then use it in your PHP code: ```php use Alltube\Config; -use Alltube\VideoDownload; +use Alltube\Video; require_once __DIR__.'/vendor/autoload.php'; -$downloader = new VideoDownload( - new Config( - [ - 'youtubedl' => '/usr/local/bin/youtube-dl', - ] - ) +Config::setOptions( + [ + 'youtubedl' => '/usr/local/bin/youtube-dl', + ] ); - -$downloader->getURL('https://www.youtube.com/watch?v=dQw4w9WgXcQ'); +$video = new Video('https://www.youtube.com/watch?v=dQw4w9WgXcQ'); +$video->getUrl(); ``` The library documentation is available on [alltube.surge.sh](https://alltube.surge.sh/classes/Alltube.VideoDownload.html). diff --git a/classes/Config.php b/classes/Config.php index 46b2340..5edf12b 100644 --- a/classes/Config.php +++ b/classes/Config.php @@ -129,16 +129,49 @@ class Config /** * Config constructor. * - * @param array $options Options (see `config/config.example.yml` for available options) + * @param array $options Options */ - public function __construct(array $options) + private function __construct(array $options = []) + { + $this->applyOptions($options); + $this->getEnv(); + $this->validateOptions(); + } + + /** + * Throw an exception if some of the options are invalid. + * + * @return void + * @throws Exception If youtube-dl is missing + * @throws Exception If Python is missing + */ + private function validateOptions() + { + /* + We don't translate these exceptions because they usually occur before Slim can catch them + so they will go to the logs. + */ + if (!is_file($this->youtubedl)) { + throw new Exception("Can't find youtube-dl at ".$this->youtubedl); + } elseif (!Video::checkCommand([$this->python, '--version'])) { + throw new Exception("Can't find Python at ".$this->python); + } + } + + /** + * Apply the provided options. + * + * @param array $options Options + * + * @return void + */ + private function applyOptions(array $options) { foreach ($options as $option => $value) { if (isset($this->$option) && isset($value)) { $this->$option = $value; } } - $this->getEnv(); } /** @@ -159,34 +192,51 @@ class Config } /** - * Get Config singleton instance from YAML config file. - * - * @param string $yamlfile YAML config file name + * Get Config singleton instance. * * @return Config */ - public static function getInstance($yamlfile = 'config/config.yml') + public static function getInstance() { - $yamlPath = __DIR__.'/../'.$yamlfile; - if (is_null(self::$instance) || self::$instance->file != $yamlfile) { - if (is_file($yamlfile)) { - $options = Yaml::parse(file_get_contents($yamlPath)); - } elseif ($yamlfile == 'config/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 - */ - $options = []; - } else { - throw new Exception("Can't find config file at ".$yamlPath); - } - self::$instance = new self($options); - self::$instance->file = $yamlfile; + if (!isset(self::$instance)) { + self::$instance = new self(); } return self::$instance; } + /** + * Set options from a YAML file. + * + * @param string $file Path to the YAML file + */ + public static function setFile($file) + { + if (is_file($file)) { + $options = Yaml::parse(file_get_contents($file)); + self::$instance = new self($options); + } else { + throw new Exception("Can't find config file at ".$file); + } + } + + /** + * Manually set some options. + * + * @param array $options Options (see `config/config.example.yml` for available options) + * @param boolean $update True to update an existing instance + */ + public static function setOptions(array $options, $update = true) + { + if ($update) { + $config = self::getInstance(); + $config->applyOptions($options); + $config->validateOptions(); + } else { + self::$instance = new self($options); + } + } + /** * Destroy singleton instance. * diff --git a/classes/PlaylistArchiveStream.php b/classes/PlaylistArchiveStream.php index d25a736..a702de1 100644 --- a/classes/PlaylistArchiveStream.php +++ b/classes/PlaylistArchiveStream.php @@ -6,7 +6,7 @@ namespace Alltube; use Barracuda\ArchiveStream\TarArchive; -use GuzzleHttp\Client; +use GuzzleHttp\Psr7\Stream; use Psr\Http\Message\StreamInterface; use RuntimeException; use stdClass; @@ -21,7 +21,7 @@ class PlaylistArchiveStream extends TarArchive implements StreamInterface /** * videos to add in the archive. * - * @var PlaylistArchiveVideo[] + * @var Video[] */ private $videos = []; @@ -32,53 +32,32 @@ class PlaylistArchiveStream extends TarArchive implements StreamInterface */ private $buffer; - /** - * Guzzle client. - * - * @var Client - */ - private $client; - - /** - * VideoDownload instance. - * - * @var VideoDownload - */ - private $download; - /** * Current video being streamed to the archive. * - * @var int + * @var Stream */ - private $curVideo; + private $curVideoStream; /** - * Video format to download. - * - * @var string + * True if the archive is complete. + * @var bool */ - private $format; + private $isComplete = false; /** * PlaylistArchiveStream constructor. * - * @param Config $config Config instance. - * @param stdClass $video Video object returned by youtube-dl - * @param string $format Requested format + * @param Video $video Video/playlist to download */ - public function __construct(Config $config, stdClass $video, $format) + public function __construct(Video $video) { - $this->client = new Client(); - $this->download = new VideoDownload($config); - - $this->format = $format; $buffer = fopen('php://temp', 'r+'); if ($buffer !== false) { $this->buffer = $buffer; } foreach ($video->entries as $entry) { - $this->videos[] = new PlaylistArchiveVideo($entry->url); + $this->videos[] = new Video($entry->url); } } @@ -91,26 +70,27 @@ class PlaylistArchiveStream extends TarArchive implements StreamInterface */ protected function send($data) { - $pos = ftell($this->buffer); + $pos = $this->tell(); - // Add data to the buffer. - fwrite($this->buffer, $data); + // Add data to the end of the buffer. + $this->seek(0, SEEK_END); + $this->write($data); if ($pos !== false) { // Rewind so that read() can later read this data. - fseek($this->buffer, $pos); + $this->seek($pos); } } /** * Write data to the stream. * - * @param string $string The string that is to be written. + * @param string $string The string that is to be written * * @return int */ public function write($string) { - throw new RuntimeException('This stream is not writeable.'); + fwrite($this->buffer, $string); } /** @@ -129,7 +109,7 @@ class PlaylistArchiveStream extends TarArchive implements StreamInterface */ public function isSeekable() { - return false; + return true; } /** @@ -139,7 +119,7 @@ class PlaylistArchiveStream extends TarArchive implements StreamInterface */ public function rewind() { - throw new RuntimeException('This stream is not seekable.'); + rewind($this->buffer); } /** @@ -149,7 +129,7 @@ class PlaylistArchiveStream extends TarArchive implements StreamInterface */ public function isWritable() { - return false; + return true; } /** @@ -181,6 +161,15 @@ class PlaylistArchiveStream extends TarArchive implements StreamInterface */ public function getMetadata($key = null) { + $meta = stream_get_meta_data($this->buffer); + + if (!isset($key)) { + return $meta; + } + + if (isset($meta[$key])) { + return $meta[$key]; + } } /** @@ -203,13 +192,7 @@ class PlaylistArchiveStream extends TarArchive implements StreamInterface */ public function __toString() { - $string = ''; - - foreach ($this->videos as $file) { - $string .= $file->url; - } - - return $string; + return $this->getContents(); } /** @@ -232,23 +215,37 @@ class PlaylistArchiveStream extends TarArchive implements StreamInterface */ public function seek($offset, $whence = SEEK_SET) { - throw new RuntimeException('This stream is not seekable.'); + fseek($this->buffer, $offset, $whence); } /** - * Returns true if the stream is at the end of the stream. + * Returns true if the stream is at the end of the archive. * * @return bool */ public function eof() { - foreach ($this->videos as $file) { - if (!$file->complete) { - return false; - } - } + return $this->isComplete && feof($this->buffer); + } - return true; + /** + * Start streaming a new video. + * + * @param Video $video Video to stream + * + * @return void + */ + private function startVideoStream(Video $video) + { + $response = $video->getHttpResponse(); + + $this->curVideoStream = $response->getBody(); + $contentLengthHeaders = $response->getHeader('Content-Length'); + + $this->init_file_stream_transfer( + $video->getFilename(), + $contentLengthHeaders[0] + ); } /** @@ -260,30 +257,30 @@ class PlaylistArchiveStream extends TarArchive implements StreamInterface */ public function read($count) { - if (isset($this->curVideo)) { - if (isset($this->curVideo->stream)) { - if (!$this->curVideo->stream->eof()) { - $this->stream_file_part($this->curVideo->stream->read($count)); - } elseif (!$this->curVideo->complete) { + // If the archive is complete, we only read the remaining buffer. + if (!$this->isComplete) { + if (isset($this->curVideoStream)) { + if ($this->curVideoStream->eof()) { + // Stop streaming the current video. $this->complete_file_stream(); - $this->curVideo->complete = true; + + $video = next($this->videos); + if ($video) { + // Start streaming the next video. + $this->startVideoStream($video); + } else { + // No video left. + $this->finish(); + $this->isComplete = true; + } } else { - $this->curVideo = next($this->videos); + // Continue streaming the current video. + $this->stream_file_part($this->curVideoStream->read($count)); } } else { - $urls = $this->download->getURL($this->curVideo->url, $this->format); - $response = $this->client->request('GET', $urls[0], ['stream' => true]); - - $contentLengthHeaders = $response->getHeader('Content-Length'); - $this->init_file_stream_transfer( - $this->download->getFilename($this->curVideo->url, $this->format), - $contentLengthHeaders[0] - ); - - $this->curVideo->stream = $response->getBody(); + // Start streaming the first video. + $this->startVideoStream(current($this->videos)); } - } else { - $this->curVideo = current($this->videos); } return fread($this->buffer, $count); @@ -299,10 +296,8 @@ class PlaylistArchiveStream extends TarArchive implements StreamInterface if (is_resource($this->buffer)) { fclose($this->buffer); } - foreach ($this->videos as $file) { - if (is_resource($file->stream)) { - fclose($file->stream); - } + if (isset($this->curVideoStream)) { + $this->curVideoStream->close(); } } } diff --git a/classes/PlaylistArchiveVideo.php b/classes/PlaylistArchiveVideo.php deleted file mode 100644 index 4610427..0000000 --- a/classes/PlaylistArchiveVideo.php +++ /dev/null @@ -1,43 +0,0 @@ -url = $url; - } -} diff --git a/classes/VideoDownload.php b/classes/Video.php similarity index 64% rename from classes/VideoDownload.php rename to classes/Video.php index b32b4d8..4d52539 100644 --- a/classes/VideoDownload.php +++ b/classes/Video.php @@ -6,13 +6,17 @@ namespace Alltube; use Exception; +use GuzzleHttp\Client; +use GuzzleHttp\Psr7\Response; use stdClass; use Symfony\Component\Process\Process; /** * Extract info about videos. + * + * Due to the way youtube-dl, this class can also contain a playlist. */ -class VideoDownload +class Video { /** * Config instance. @@ -21,30 +25,37 @@ class VideoDownload */ private $config; + /** + * URL of the page containing the video + * @var string + */ + private $webpageUrl; + + /** + * Requested video format + * @var string + */ + private $requestedFormat; + + /** + * Password + * @var string + */ + private $password; + /** * VideoDownload constructor. * - * @param Config $config Config instance. - * - * @throws Exception If youtube-dl is missing - * @throws Exception If Python is missing + * @param string $webpageUrl URL of the page containing the video + * @param string $requestedFormat Requested video format + * @param string $password Password */ - public function __construct(Config $config = null) + public function __construct($webpageUrl, $requestedFormat = 'best', $password = null) { - if (isset($config)) { - $this->config = $config; - } else { - $this->config = Config::getInstance(); - } - /* - We don't translate these exceptions because they always occur before Slim can catch them - so they will always go to the logs. - */ - if (!is_file($this->config->youtubedl)) { - throw new Exception("Can't find youtube-dl at ".$this->config->youtubedl); - } elseif (!$this->checkCommand([$this->config->python, '--version'])) { - throw new Exception("Can't find Python at ".$this->config->python); - } + $this->webpageUrl = $webpageUrl; + $this->requestedFormat = $requestedFormat; + $this->password = $password; + $this->config = Config::getInstance(); } /** @@ -56,10 +67,11 @@ class VideoDownload */ private function getProcess(array $arguments) { + $config = Config::getInstance(); return new Process( array_merge( - [$this->config->python, $this->config->youtubedl], - $this->config->params, + [$config->python, $config->youtubedl], + $config->params, $arguments ) ); @@ -70,18 +82,15 @@ class VideoDownload * * @return string[] Extractors * */ - public function listExtractors() + public static function getExtractors() { - return explode("\n", trim($this->getProp(null, null, 'list-extractors'))); + return explode("\n", trim(self::getProp('list-extractors'))); } /** * Get a property from youtube-dl. * - * @param string $url URL to parse - * @param string $format Format * @param string $prop Property - * @param string $password Video password * * @throws PasswordException If the video is protected by a password and no password was specified * @throws Exception If the password is wrong @@ -89,23 +98,30 @@ class VideoDownload * * @return string */ - private function getProp($url = null, $format = null, $prop = 'dump-json', $password = null) + private function getProp($prop = 'dump-json') { + $config = Config::getInstance(); + $arguments = ['--'.$prop]; - if (isset($url)) { - $arguments[] = $url; - } - if (isset($format)) { - $arguments[] = '-f '.$format; - } - if (isset($password)) { - $arguments[] = '--video-password'; - $arguments[] = $password; + + // This function can also be called statically. + if (isset($this)) { + if (isset($this->webpageUrl)) { + $arguments[] = $this->webpageUrl; + } + if (isset($this->requestedFormat)) { + $arguments[] = '-f'; + $arguments[] = $this->requestedFormat; + } + if (isset($this->password)) { + $arguments[] = '--video-password'; + $arguments[] = $this->password; + } } - $process = $this->getProcess($arguments); + $process = self::getProcess($arguments); //This is needed by the openload extractor because it runs PhantomJS - $process->setEnv(['PATH'=>$this->config->phantomjsDir]); + $process->setEnv(['PATH'=>$config->phantomjsDir]); $process->inheritEnvironmentVariables(); $process->run(); if (!$process->isSuccessful()) { @@ -126,15 +142,41 @@ class VideoDownload /** * Get all information about a video. * - * @param string $url URL of page - * @param string $format Format to use for the video - * @param string $password Video password - * * @return stdClass Decoded JSON * */ - public function getJSON($url, $format = null, $password = null) + public function getJson() { - return json_decode($this->getProp($url, $format, 'dump-single-json', $password)); + if (!isset($this->json)) { + $this->json = json_decode($this->getProp('dump-single-json')); + } + + return $this->json; + } + + /** + * Magic method to get a property from the JSON object returned by youtube-dl. + * + * @param string $name Property + * + * @return mixed + */ + public function __get($name) + { + if (isset($this->$name)) { + return $this->getJson()->$name; + } + } + + /** + * Magic method to check if the JSON object returned by youtube-dl has a property. + * + * @param string $name Property + * + * @return boolean + */ + public function __isset($name) + { + return isset($this->getJson()->$name); } /** @@ -144,15 +186,11 @@ class VideoDownload * 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[] URLs of video * */ - public function getURL($url, $format = null, $password = null) + public function getUrl() { - $urls = explode("\n", $this->getProp($url, $format, 'get-url', $password)); + $urls = explode("\n", $this->getProp('get-url')); if (empty($urls[0])) { throw new EmptyUrlException(_('youtube-dl returned an empty URL.')); @@ -164,32 +202,25 @@ class VideoDownload /** * Get filename of video file from URL of page. * - * @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 * */ - public function getFilename($url, $format = null, $password = null) + public function getFilename() { - return trim($this->getProp($url, $format, 'get-filename', $password)); + return trim($this->getProp('get-filename')); } /** * 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) + public function getFileNameWithExtension($extension) { return html_entity_decode( pathinfo( - $this->getFilename($url, $format, $password), + $this->getFilename(), PATHINFO_FILENAME ).'.'.$extension, ENT_COMPAT, @@ -197,32 +228,16 @@ class VideoDownload ); } - /** - * Get filename of audio from URL of page. - * - * @param string $url URL of page - * @param string $format Format to use for the video - * @param string $password Video password - * - * @return string Filename of converted audio file - * */ - public function getAudioFilename($url, $format = null, $password = null) - { - return $this->getFileNameWithExtension('mp3', $url, $format, $password); - } - /** * Return arguments used to run rtmp for a specific video. * - * @param stdClass $video Video object returned by youtube-dl - * * @return array Arguments */ - private function getRtmpArguments(stdClass $video) + private function getRtmpArguments() { $arguments = []; - if ($video->protocol == 'rtmp') { + if ($this->protocol == 'rtmp') { foreach ([ 'url' => '-rtmp_tcurl', 'webpage_url' => '-rtmp_pageurl', @@ -231,14 +246,14 @@ class VideoDownload 'play_path' => '-rtmp_playpath', 'app' => '-rtmp_app', ] as $property => $option) { - if (isset($video->{$property})) { + if (isset($this->{$property})) { $arguments[] = $option; - $arguments[] = $video->{$property}; + $arguments[] = $this->{$property}; } } - if (isset($video->rtmp_conn)) { - foreach ($video->rtmp_conn as $conn) { + if (isset($this->rtmp_conn)) { + foreach ($this->rtmp_conn as $conn) { $arguments[] = '-rtmp_conn'; $arguments[] = $conn; } @@ -255,7 +270,7 @@ class VideoDownload * * @return bool False if the command returns an error, true otherwise */ - private function checkCommand(array $command) + public static function checkCommand(array $command) { $process = new Process($command); $process->run(); @@ -266,7 +281,6 @@ class VideoDownload /** * Get a process that runs avconv in order to convert a video. * - * @param stdClass $video Video object returned by youtube-dl * @param int $audioBitrate Audio bitrate of the converted file * @param string $filetype Filetype of the converted file * @param bool $audioOnly True to return an audio-only file @@ -278,7 +292,6 @@ class VideoDownload * @return Process Process */ private function getAvconvProcess( - stdClass $video, $audioBitrate, $filetype = 'mp3', $audioOnly = true, @@ -317,9 +330,9 @@ class VideoDownload $this->config->avconv, '-v', $this->config->avconvVerbosity, ], - $this->getRtmpArguments($video), + $this->getRtmpArguments(), [ - '-i', $video->url, + '-i', $this->url, '-f', $filetype, '-b:a', $audioBitrate.'k', ], @@ -328,10 +341,10 @@ class VideoDownload 'pipe:1', ] ); - if ($video->url != '-') { + if ($this->url != '-') { //Vimeo needs a correct user-agent $arguments[] = '-user_agent'; - $arguments[] = $this->getProp(null, null, 'dump-user-agent'); + $arguments[] = $this->getProp('dump-user-agent'); } return new Process($arguments); @@ -340,34 +353,29 @@ class VideoDownload /** * Get audio stream of converted video. * - * @param string $url URL of page - * @param string $format Format to use for the video - * @param string $password Video password * @param string $from Start the conversion at this time * @param string $to End the conversion at this time * - * @throws Exception If your try to convert and M3U8 video + * @throws Exception If your try to convert an M3U8 video * @throws Exception If the popen stream was not created correctly * * @return resource popen stream */ - public function getAudioStream($url, $format, $password = null, $from = null, $to = null) + public function getAudioStream($from = null, $to = null) { - $video = $this->getJSON($url, $format, $password); - - if (isset($video->_type) && $video->_type == 'playlist') { + if (isset($this->_type) && $this->_type == 'playlist') { throw new Exception(_('Conversion of playlists is not supported.')); } - if (isset($video->protocol)) { - if (in_array($video->protocol, ['m3u8', 'm3u8_native'])) { + if (isset($this->protocol)) { + if (in_array($this->protocol, ['m3u8', 'm3u8_native'])) { throw new Exception(_('Conversion of M3U8 files is not supported.')); - } elseif ($video->protocol == 'http_dash_segments') { + } elseif ($this->protocol == 'http_dash_segments') { throw new Exception(_('Conversion of DASH segments is not supported.')); } } - $avconvProc = $this->getAvconvProcess($video, $this->config->audioBitrate, 'mp3', true, $from, $to); + $avconvProc = $this->getAvconvProcess($this->config->audioBitrate, 'mp3', true, $from, $to); $stream = popen($avconvProc->getCommandLine(), 'r'); @@ -381,14 +389,12 @@ class VideoDownload /** * Get video stream from an M3U playlist. * - * @param stdClass $video Video object returned by getJSON - * * @throws Exception If avconv/ffmpeg is missing * @throws Exception If the popen stream was not created correctly * * @return resource popen stream */ - public function getM3uStream(stdClass $video) + public function getM3uStream() { if (!$this->checkCommand([$this->config->avconv, '-version'])) { throw new Exception(_('Can\'t find avconv or ffmpeg at ').$this->config->avconv.'.'); @@ -398,8 +404,8 @@ class VideoDownload [ $this->config->avconv, '-v', $this->config->avconvVerbosity, - '-i', $video->url, - '-f', $video->ext, + '-i', $this->url, + '-f', $this->ext, '-c', 'copy', '-bsf:a', 'aac_adtstoasc', '-movflags', 'frag_keyframe+empty_moov', @@ -418,14 +424,18 @@ class VideoDownload /** * Get an avconv stream to remux audio and video. * - * @param array $urls URLs of the video ($urls[0]) and audio ($urls[1]) files - * * @throws Exception If the popen stream was not created correctly * * @return resource popen stream */ - public function getRemuxStream(array $urls) + public function getRemuxStream() { + $urls = $this->getUrl(); + + if (!isset($urls[0]) || !isset($urls[1])) { + throw new Exception(_('This video does not have two URLs.')); + } + $process = new Process( [ $this->config->avconv, @@ -451,13 +461,11 @@ class VideoDownload /** * Get video stream from an RTMP video. * - * @param stdClass $video Video object returned by getJSON - * * @throws Exception If the popen stream was not created correctly * * @return resource popen stream */ - public function getRtmpStream(stdClass $video) + public function getRtmpStream() { $process = new Process( array_merge( @@ -465,10 +473,10 @@ class VideoDownload $this->config->avconv, '-v', $this->config->avconvVerbosity, ], - $this->getRtmpArguments($video), + $this->getRtmpArguments(), [ - '-i', $video->url, - '-f', $video->ext, + '-i', $this->url, + '-f', $this->ext, 'pipe:1', ] ) @@ -484,25 +492,21 @@ class VideoDownload /** * Get the stream of a converted video. * - * @param string $url URL of page - * @param string $format Source format to use for the conversion * @param int $audioBitrate Audio bitrate of the converted file * @param string $filetype Filetype of the converted file - * @param string $password Video password * * @throws Exception If your try to convert and M3U8 video * @throws Exception If the popen stream was not created correctly * * @return resource popen stream */ - public function getConvertedStream($url, $format, $audioBitrate, $filetype, $password = null) + public function getConvertedStream($audioBitrate, $filetype) { - $video = $this->getJSON($url, $format, $password); - if (in_array($video->protocol, ['m3u8', 'm3u8_native'])) { + if (in_array($this->protocol, ['m3u8', 'm3u8_native'])) { throw new Exception(_('Conversion of M3U8 files is not supported.')); } - $avconvProc = $this->getAvconvProcess($video, $audioBitrate, $filetype, false); + $avconvProc = $this->getAvconvProcess($audioBitrate, $filetype, false); $stream = popen($avconvProc->getCommandLine(), 'r'); @@ -512,4 +516,29 @@ class VideoDownload return $stream; } + + /** + * Get the same video but with another format. + * + * @param string $format New format + * + * @return Video + */ + public function withFormat($format) + { + return new Video($this->webpageUrl, $format, $this->password); + } + + /** + * Get a HTTP response containing the video. + * + * @return Response + */ + public function getHttpResponse() + { + $client = new Client(); + $urls = $this->getUrl(); + + return $client->request('GET', $urls[0], ['stream' => true]); + } } diff --git a/controllers/FrontController.php b/controllers/FrontController.php index 9789583..f7cee45 100644 --- a/controllers/FrontController.php +++ b/controllers/FrontController.php @@ -11,7 +11,7 @@ use Alltube\Locale; use Alltube\LocaleManager; use Alltube\PasswordException; use Alltube\PlaylistArchiveStream; -use Alltube\VideoDownload; +use Alltube\Video; use Aura\Session\Segment; use Aura\Session\SessionFactory; use Exception; @@ -36,11 +36,11 @@ class FrontController private $config; /** - * VideoDownload instance. + * Current video. * - * @var VideoDownload + * @var Video */ - private $download; + private $video; /** * Slim dependency container. @@ -81,17 +81,11 @@ class FrontController * FrontController constructor. * * @param ContainerInterface $container Slim dependency container - * @param Config $config Config instance * @param array $cookies Cookie array */ - public function __construct(ContainerInterface $container, Config $config = null, array $cookies = []) + public function __construct(ContainerInterface $container, array $cookies = []) { - if (isset($config)) { - $this->config = $config; - } else { - $this->config = Config::getInstance(); - } - $this->download = new VideoDownload($this->config); + $this->config = Config::getInstance(); $this->container = $container; $this->view = $this->container->get('view'); $this->localeManager = $this->container->get('locale'); @@ -162,7 +156,7 @@ class FrontController 'extractors.tpl', [ 'config' => $this->config, - 'extractors' => $this->download->listExtractors(), + 'extractors' => Video::getExtractors(), 'class' => 'extractors', 'title' => _('Supported websites'), 'description' => _('List of all supported websites from which Alltube Download '. @@ -206,44 +200,28 @@ class FrontController * * @param Request $request PSR-7 request * @param Response $response PSR-7 response - * @param array $params GET query parameters - * @param string $password Video password * * @return Response HTTP response */ - private function getConvertedAudioResponse(Request $request, Response $response, array $params, $password = null) + private function getConvertedAudioResponse(Request $request, Response $response) { - if (!isset($params['from'])) { - $params['from'] = ''; - } - if (!isset($params['to'])) { - $params['to'] = ''; - } + $from = $request->getQueryParam('from'); + $to = $request->getQueryParam('to'); $response = $response->withHeader( 'Content-Disposition', 'attachment; filename="'. - $this->download->getAudioFilename($params['url'], 'bestaudio/best', $password).'"' + $this->video->getFileNameWithExtension('mp3').'"' ); $response = $response->withHeader('Content-Type', 'audio/mpeg'); if ($request->isGet() || $request->isPost()) { try { - $process = $this->download->getAudioStream( - $params['url'], - 'bestaudio/best', - $password, - $params['from'], - $params['to'] - ); + $process = $this->video->getAudioStream($from, $to); } catch (Exception $e) { - $process = $this->download->getAudioStream( - $params['url'], - $this->defaultFormat, - $password, - $params['from'], - $params['to'] - ); + // Fallback to default format. + $this->video = $this->video->withFormat($this->defaultFormat); + $process = $this->video->getAudioStream($from, $to); } $response = $response->withBody(new Stream($process)); } @@ -256,31 +234,35 @@ class FrontController * * @param Request $request PSR-7 request * @param Response $response PSR-7 response - * @param array $params GET query parameters - * @param string $password Video password * * @return Response HTTP response */ - private function getAudioResponse(Request $request, Response $response, array $params, $password = null) + private function getAudioResponse(Request $request, Response $response) { try { - if ((isset($params['from']) && !empty($params['from'])) - || (isset($params['to']) && !empty($params['to'])) - ) { + // First, we try to get a MP3 file directly. + if (!empty($request->getQueryParam('from')) || !empty($request->getQueryParam('to'))) { throw new Exception('Force convert when we need to seek.'); } if ($this->config->stream) { - return $this->getStream($params['url'], 'mp3', $response, $request, $password); + $this->video = $this->video->withFormat('mp3'); + + return $this->getStream($request, $response); } else { - $urls = $this->download->getURL($params['url'], 'mp3[protocol=https]/mp3[protocol=http]', $password); + $this->video = $this->video->withFormat('mp3[protocol=https]/mp3[protocol=http]'); + + $urls = $this->video->getUrl(); return $response->withRedirect($urls[0]); } } catch (PasswordException $e) { return $this->password($request, $response); } catch (Exception $e) { - return $this->getConvertedAudioResponse($request, $response, $params, $password); + // If MP3 is not available, we convert it. + $this->video = $this->video->withFormat($this->defaultFormat); + + return $this->getConvertedAudioResponse($request, $response); } } @@ -289,35 +271,33 @@ class FrontController * * @param Request $request PSR-7 request * @param Response $response PSR-7 response - * @param array $params GET query parameters - * @param string $password Video password * * @return Response HTTP response */ - private function getVideoResponse(Request $request, Response $response, array $params, $password = null) + private function getVideoResponse(Request $request, Response $response) { try { - $video = $this->download->getJSON($params['url'], $this->defaultFormat, $password); + $this->video->getJson(); } catch (PasswordException $e) { return $this->password($request, $response); } - if (isset($video->entries)) { + if (isset($this->video->entries)) { $template = 'playlist.tpl'; } else { $template = 'video.tpl'; } $title = _('Video download'); - $description = _('Download video from ').$video->extractor_key; - if (isset($video->title)) { - $title = $video->title; - $description = _('Download').' "'.$video->title.'" '._('from').' '.$video->extractor_key; + $description = _('Download video from ').$this->video->extractor_key; + if (isset($this->video->title)) { + $title = $this->video->title; + $description = _('Download').' "'.$this->video->title.'" '._('from').' '.$this->video->extractor_key; } $this->view->render( $response, $template, [ - 'video' => $video, + 'video' => $this->video, 'class' => 'video', 'title' => $title, 'description' => $description, @@ -341,21 +321,20 @@ class FrontController */ public function video(Request $request, Response $response) { - $params = $request->getQueryParams(); + $url = $request->getQueryParam('url') ?: $request->getQueryParam('v'); - if (!isset($params['url']) && isset($params['v'])) { - $params['url'] = $params['v']; - } - - if (isset($params['url']) && !empty($params['url'])) { + if (isset($url) && !empty($url)) { $password = $request->getParam('password'); if (isset($password)) { - $this->sessionSegment->setFlash($params['url'], $password); + $this->sessionSegment->setFlash($url, $password); } - if (isset($params['audio'])) { - return $this->getAudioResponse($request, $response, $params, $password); + + $this->video = new Video($url, $this->defaultFormat, $password); + + if ($request->getQueryParam('audio')) { + return $this->getAudioResponse($request, $response); } else { - return $this->getVideoResponse($request, $response, $params, $password); + return $this->getVideoResponse($request, $response); } } else { return $response->withRedirect($this->container->get('router')->pathFor('index')); @@ -392,39 +371,33 @@ class FrontController /** * 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 HTTP response */ - private function getStream($url, $format, Response $response, Request $request, $password = null) + private function getStream(Request $request, Response $response) { - $video = $this->download->getJSON($url, $format, $password); - if (isset($video->entries)) { - $stream = new PlaylistArchiveStream($this->config, $video, $format); + if (isset($this->video->entries)) { + $stream = new PlaylistArchiveStream($this->video); $response = $response->withHeader('Content-Type', 'application/x-tar'); $response = $response->withHeader( 'Content-Disposition', - 'attachment; filename="'.$video->title.'.tar"' + 'attachment; filename="'.$this->video->title.'.tar"' ); return $response->withBody($stream); - } elseif ($video->protocol == 'rtmp') { - $stream = $this->download->getRtmpStream($video); - $response = $response->withHeader('Content-Type', 'video/'.$video->ext); - $body = new Stream($stream); - } elseif ($video->protocol == 'm3u8' || $video->protocol == 'm3u8_native') { - $stream = $this->download->getM3uStream($video); - $response = $response->withHeader('Content-Type', 'video/'.$video->ext); - $body = new Stream($stream); + } elseif ($this->video->protocol == 'rtmp') { + $response = $response->withHeader('Content-Type', 'video/'.$this->video->ext); + $body = new Stream($this->video->getRtmpStream()); + } elseif ($this->video->protocol == 'm3u8' || $this->video->protocol == 'm3u8_native') { + $response = $response->withHeader('Content-Type', 'video/'.$this->video->ext); + $body = new Stream($this->video->getM3uStream()); } else { $client = new Client(); $stream = $client->request( 'GET', - $video->url, + $this->video->url, [ 'stream' => true, 'headers' => ['Range' => $request->getHeader('Range')], @@ -445,7 +418,7 @@ class FrontController $response = $response->withHeader( 'Content-Disposition', 'attachment; filename="'. - $this->download->getFilename($url, $format, $password).'"' + $this->video->getFilename().'"' ); return $response; @@ -454,19 +427,17 @@ class FrontController /** * Get a remuxed stream piped through the server. * - * @param string[] $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) + private function getRemuxStream(Request $request, Response $response) { if (!$this->config->remux) { throw new Exception(_('You need to enable remux mode to merge two formats.')); } - $stream = $this->download->getRemuxStream($urls); + $stream = $this->video->getRemuxStream(); $response = $response->withHeader('Content-Type', 'video/x-matroska'); if ($request->isGet()) { $response = $response->withBody(new Stream($stream)); @@ -475,12 +446,7 @@ class FrontController return $response->withHeader( 'Content-Disposition', - 'attachment; filename="'.$this->download->getFileNameWithExtension( - 'mkv', - $webpageUrl, - $format, - $this->sessionSegment->getFlash($webpageUrl) - ) + 'attachment; filename="'.$this->video->getFileNameWithExtension('mkv') ); } @@ -505,21 +471,15 @@ class FrontController * 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) + private function getRedirectResponse(Request $request, Response $response) { try { - $videoUrls = $this->download->getURL( - $url, - $format, - $this->sessionSegment->getFlash($url) - ); + $videoUrls = $this->video->getUrl(); } catch (EmptyUrlException $e) { /* If this happens it is probably a playlist @@ -528,15 +488,9 @@ class FrontController $videoUrls = []; } if (count($videoUrls) > 1) { - return $this->getRemuxStream($videoUrls, $format, $response, $request); + return $this->getRemuxStream($request, $response); } elseif ($this->config->stream) { - return $this->getStream( - $url, - $format, - $response, - $request, - $this->sessionSegment->getFlash($url) - ); + return $this->getStream($request, $response); } else { if (empty($videoUrls[0])) { throw new Exception(_("Can't find URL of video.")); @@ -551,33 +505,22 @@ class FrontController * * @param Request $request PSR-7 request * @param Response $response PSR-7 response - * @param array $params GET query parameters - * @param string $format Requested source format * * @return Response HTTP response */ - private function getConvertedResponse(Request $request, Response $response, array $params, $format) + private function getConvertedResponse(Request $request, Response $response) { - $password = $request->getParam('password'); $response = $response->withHeader( 'Content-Disposition', 'attachment; filename="'. - $this->download->getFileNameWithExtension( - $params['customFormat'], - $params['url'], - $format, - $password - ).'"' + $this->video->getFileNameWithExtension($request->getQueryParam('customFormat')).'"' ); - $response = $response->withHeader('Content-Type', 'video/'.$params['customFormat']); + $response = $response->withHeader('Content-Type', 'video/'.$request->getQueryParam('customFormat')); if ($request->isGet() || $request->isPost()) { - $process = $this->download->getConvertedStream( - $params['url'], - $format, - $params['customBitrate'], - $params['customFormat'], - $password + $process = $this->video->getConvertedStream( + $request->getQueryParam('customBitrate'), + $request->getQueryParam('customFormat') ); $response = $response->withBody(new Stream($process)); } @@ -595,18 +538,21 @@ class FrontController */ public function redirect(Request $request, Response $response) { - $params = $request->getQueryParams(); $format = $this->getFormat($request); - if (isset($params['url'])) { + $url = $request->getQueryParam('url'); + + if (isset($url)) { + $this->video = new Video($url, $format, $this->sessionSegment->getFlash($url)); + try { if ($this->config->convertAdvanced && !is_null($request->getQueryParam('customConvert'))) { - return $this->getConvertedResponse($request, $response, $params, $format); + return $this->getConvertedResponse($request, $response); } - return $this->getRedirectResponse($params['url'], $format, $response, $request); + return $this->getRedirectResponse($request, $response); } 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()); @@ -628,16 +574,14 @@ class FrontController */ public function json(Request $request, Response $response) { - $params = $request->getQueryParams(); $format = $this->getFormat($request); - if (isset($params['url'])) { + $url = $request->getQueryParam('url'); + + if (isset($url)) { try { - return $response->withJson( - $this->download->getJSON( - $params['url'], - $format - ) - ); + $this->video = new Video($url, $format); + + return $response->withJson($this->video->getJson()); } catch (Exception $e) { return $response->withJson(['error' => $e->getMessage()]) ->withStatus(500); diff --git a/index.php b/index.php index c42b96e..77f8a85 100644 --- a/index.php +++ b/index.php @@ -14,6 +14,10 @@ if (isset($_SERVER['REQUEST_URI']) && strpos($_SERVER['REQUEST_URI'], '/index.ph die; } +if (is_file(__DIR__.'/config/config.yml')) { + Config::setFile(__DIR__.'/config/config.yml'); +} + $app = new App(); $container = $app->getContainer(); $config = Config::getInstance(); @@ -28,7 +32,7 @@ if (!class_exists('Locale')) { $container['locale'] = new LocaleManager($_COOKIE); $app->add(new LocaleMiddleware($container)); -$controller = new FrontController($container, null, $_COOKIE); +$controller = new FrontController($container, $_COOKIE); $container['errorHandler'] = [$controller, 'error']; diff --git a/tests/BaseTest.php b/tests/BaseTest.php new file mode 100644 index 0000000..c70756b --- /dev/null +++ b/tests/BaseTest.php @@ -0,0 +1,42 @@ +config = Config::getInstance('config/config_test.yml'); - } - - /** - * Destroy variables created by setUp(). - * - * @return void - */ - protected function tearDown() - { - Config::destroyInstance(); + $this->config = Config::getInstance(); } /** @@ -70,27 +60,79 @@ class ConfigTest extends TestCase } /** - * Test the getInstance function with a missing config file. + * Test the setFile function. + * + * @return void + */ + public function testSetFile() + { + if (PHP_OS == 'WINNT') { + $configFile = 'config_test_windows.yml'; + } else { + $configFile = 'config_test.yml'; + } + + $this->assertNull(Config::setFile(__DIR__.'/../config/'.$configFile)); + } + + /** + * Test the setFile function with a missing config file. * * @return void * @expectedException Exception */ - public function testGetInstanceWithMissingFile() + public function testSetFileWithMissingFile() { - Config::getInstance('foo'); + Config::setFile('foo'); } /** - * Test the getInstance function with an empty filename. + * Test the setOptions function. * * @return void */ - public function testGetInstanceWithEmptyFile() + public function testSetOptions() { - $config = Config::getInstance(''); - $this->assertConfig($config); + Config::setOptions(['appName' => 'foo']); + $config = Config::getInstance(); + $this->assertEquals($config->appName, 'foo'); } + /** + * Test the setOptions function. + * + * @return void + */ + public function testSetOptionsWithoutUpdate() + { + Config::setOptions(['appName' => 'foo'], false); + $config = Config::getInstance(); + $this->assertEquals($config->appName, 'foo'); + } + + /** + * Test the setOptions function. + * + * @return void + * @expectedException Exception + */ + public function testSetOptionsWithBadYoutubedl() + { + Config::setOptions(['youtubedl' => 'foo']); + } + + /** + * Test the setOptions function. + * + * @return void + * @expectedException Exception + */ + public function testSetOptionsWithBadPython() + { + Config::setOptions(['python' => 'foo']); + } + + /** * Test the getInstance function with the CONVERT and PYTHON environment variables. * @@ -100,11 +142,8 @@ class ConfigTest extends TestCase { Config::destroyInstance(); putenv('CONVERT=1'); - putenv('PYTHON=foo'); - $config = Config::getInstance('config/config_test.yml'); + $config = Config::getInstance(); $this->assertEquals($config->convert, true); - $this->assertEquals($config->python, 'foo'); putenv('CONVERT'); - putenv('PYTHON'); } } diff --git a/tests/FrontControllerTest.php b/tests/FrontControllerTest.php index a7a9c21..f8cfa02 100644 --- a/tests/FrontControllerTest.php +++ b/tests/FrontControllerTest.php @@ -19,7 +19,7 @@ use Slim\Http\Response; /** * Unit tests for the FrontController class. */ -class FrontControllerTest extends TestCase +class FrontControllerTest extends BaseTest { /** * Slim dependency container. @@ -61,19 +61,15 @@ class FrontControllerTest extends TestCase */ protected function setUp() { + parent::setUp(); + $this->container = new Container(); $this->request = Request::createFromEnvironment(Environment::mock()); $this->response = new Response(); $this->container['view'] = ViewFactory::create($this->container, $this->request); $this->container['locale'] = new LocaleManager(); - if (PHP_OS == 'WINNT') { - $configFile = 'config_test_windows.yml'; - } else { - $configFile = 'config_test.yml'; - } - $this->config = Config::getInstance('config/'.$configFile); - $this->controller = new FrontController($this->container, $this->config); + $this->controller = new FrontController($this->container); $this->container['router']->map(['GET'], '/', [$this->controller, 'index']) ->setName('index'); @@ -87,32 +83,17 @@ class FrontControllerTest extends TestCase ->setName('locale'); } - /** - * Destroy properties after test. - */ - protected function tearDown() - { - 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) + private function getRequestResult($request, array $params) { - if (isset($config)) { - $controller = new FrontController($this->container, $config); - } else { - $controller = $this->controller; - } - - return $controller->$request( + return $this->controller->$request( $this->request->withQueryParams($params), $this->response ); @@ -123,13 +104,12 @@ class FrontControllerTest extends TestCase * * @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) + private function assertRequestIsOk($request, array $params = []) { - $this->assertTrue($this->getRequestResult($request, $params, $config)->isOk()); + $this->assertTrue($this->getRequestResult($request, $params)->isOk()); } /** @@ -137,13 +117,12 @@ class FrontControllerTest extends TestCase * * @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) + private function assertRequestIsRedirect($request, array $params = []) { - $this->assertTrue($this->getRequestResult($request, $params, $config)->isRedirect()); + $this->assertTrue($this->getRequestResult($request, $params)->isRedirect()); } /** @@ -151,13 +130,12 @@ class FrontControllerTest extends TestCase * * @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) + private function assertRequestIsServerError($request, array $params = []) { - $this->assertTrue($this->getRequestResult($request, $params, $config)->isServerError()); + $this->assertTrue($this->getRequestResult($request, $params)->isServerError()); } /** @@ -165,13 +143,12 @@ class FrontControllerTest extends TestCase * * @param string $request Controller function to call * @param array $params Query parameters - * @param Config $config Custom config * * @return void */ - private function assertRequestIsClientError($request, array $params = [], Config $config = null) + private function assertRequestIsClientError($request, array $params = []) { - $this->assertTrue($this->getRequestResult($request, $params, $config)->isClientError()); + $this->assertTrue($this->getRequestResult($request, $params)->isClientError()); } /** @@ -181,20 +158,7 @@ class FrontControllerTest extends TestCase */ public function testConstructor() { - $controller = new FrontController($this->container, $this->config); - $this->assertInstanceOf(FrontController::class, $controller); - } - - /** - * Test the constructor with a default config. - * - * @return void - * @requires OS Linux - */ - public function testConstructorWithDefaultConfig() - { - $controller = new FrontController($this->container); - $this->assertInstanceOf(FrontController::class, $controller); + $this->assertInstanceOf(FrontController::class, new FrontController($this->container)); } /** @@ -204,9 +168,8 @@ class FrontControllerTest extends TestCase */ public function testConstructorWithStream() { - $this->config->stream = true; - $controller = new FrontController($this->container, $this->config); - $this->assertInstanceOf(FrontController::class, $controller); + Config::setOptions(['stream' => true]); + $this->assertInstanceOf(FrontController::class, new FrontController($this->container)); } /** @@ -354,12 +317,12 @@ class FrontControllerTest extends TestCase */ public function testVideoWithStream() { - $this->config->stream = true; - $this->assertRequestIsOk('video', ['url' => 'https://www.youtube.com/watch?v=M7IpKCZ47pU'], $this->config); + Config::setOptions(['stream' => true]); + + $this->assertRequestIsOk('video', ['url' => 'https://www.youtube.com/watch?v=M7IpKCZ47pU']); $this->assertRequestIsOk( 'video', - ['url' => 'https://www.youtube.com/watch?v=M7IpKCZ47pU', 'audio' => true], - $this->config + ['url' => 'https://www.youtube.com/watch?v=M7IpKCZ47pU', 'audio' => true] ); } @@ -427,11 +390,11 @@ class FrontControllerTest extends TestCase */ public function testRedirectWithStream() { - $this->config->stream = true; + Config::setOptions(['stream' => true]); + $this->assertRequestIsOk( 'redirect', - ['url' => 'https://www.youtube.com/watch?v=M7IpKCZ47pU'], - $this->config + ['url' => 'https://www.youtube.com/watch?v=M7IpKCZ47pU'] ); } @@ -445,14 +408,15 @@ class FrontControllerTest extends TestCase if (getenv('CI')) { $this->markTestSkipped('Twitter returns a 429 error when the test is ran too many times.'); } - $this->config->stream = true; + + Config::setOptions(['stream' => true]); + $this->assertRequestIsOk( 'redirect', [ 'url' => 'https://twitter.com/verge/status/813055465324056576/video/1', 'format' => 'hls-2176', - ], - $this->config + ] ); } @@ -465,11 +429,11 @@ class FrontControllerTest extends TestCase { $this->markTestIncomplete('We need to find another RTMP video.'); - $this->config->stream = true; + Config::setOptions(['stream' => true]); + $this->assertRequestIsOk( 'redirect', - ['url' => 'http://www.rtvnh.nl/video/131946', 'format' => 'rtmp-264'], - $this->config + ['url' => 'http://www.rtvnh.nl/video/131946', 'format' => 'rtmp-264'] ); } @@ -480,14 +444,14 @@ class FrontControllerTest extends TestCase */ public function testRedirectWithRemux() { - $this->config->remux = true; + Config::setOptions(['remux' => true]); + $this->assertRequestIsOk( 'redirect', [ 'url' => 'https://www.youtube.com/watch?v=M7IpKCZ47pU', 'format' => 'bestvideo+bestaudio', - ], - $this->config + ] ); } @@ -552,11 +516,11 @@ class FrontControllerTest extends TestCase */ public function testRedirectWithPlaylist() { - $this->config->stream = true; + Config::setOptions(['stream' => true]); + $this->assertRequestIsOk( 'redirect', - ['url' => 'https://www.youtube.com/playlist?list=PLgdySZU6KUXL_8Jq5aUkyNV7wCa-4wZsC'], - $this->config + ['url' => 'https://www.youtube.com/playlist?list=PLgdySZU6KUXL_8Jq5aUkyNV7wCa-4wZsC'] ); } @@ -567,7 +531,8 @@ class FrontControllerTest extends TestCase */ public function testRedirectWithAdvancedConversion() { - $this->config->convertAdvanced = true; + Config::setOptions(['convertAdvanced' => true]); + $this->assertRequestIsOk( 'redirect', [ @@ -576,8 +541,7 @@ class FrontControllerTest extends TestCase 'customConvert' => 'on', 'customBitrate' => 32, 'customFormat' => 'flv', - ], - $this->config + ] ); } diff --git a/tests/LocaleManagerTest.php b/tests/LocaleManagerTest.php index 9bf9837..aab0463 100644 --- a/tests/LocaleManagerTest.php +++ b/tests/LocaleManagerTest.php @@ -12,7 +12,7 @@ use PHPUnit\Framework\TestCase; /** * Unit tests for the LocaleManagerTest class. */ -class LocaleManagerTest extends TestCase +class LocaleManagerTest extends BaseTest { /** * LocaleManager class instance. diff --git a/tests/LocaleMiddlewareTest.php b/tests/LocaleMiddlewareTest.php index f7455fd..a8f45b4 100644 --- a/tests/LocaleMiddlewareTest.php +++ b/tests/LocaleMiddlewareTest.php @@ -17,7 +17,7 @@ use Slim\Http\Response; /** * Unit tests for the FrontController class. */ -class LocaleMiddlewareTest extends TestCase +class LocaleMiddlewareTest extends BaseTest { /** * LocaleMiddleware instance. diff --git a/tests/LocaleTest.php b/tests/LocaleTest.php index ba18b84..f425fe0 100644 --- a/tests/LocaleTest.php +++ b/tests/LocaleTest.php @@ -11,7 +11,7 @@ use PHPUnit\Framework\TestCase; /** * Unit tests for the LocaleTest class. */ -class LocaleTest extends TestCase +class LocaleTest extends BaseTest { /** * Locale class instance. diff --git a/tests/PlaylistArchiveStreamTest.php b/tests/PlaylistArchiveStreamTest.php index 0f07b2e..fc34fe3 100644 --- a/tests/PlaylistArchiveStreamTest.php +++ b/tests/PlaylistArchiveStreamTest.php @@ -6,6 +6,7 @@ namespace Alltube\Test; use Alltube\Config; +use Alltube\Video; use Alltube\PlaylistArchiveStream; use PHPUnit\Framework\TestCase; use RuntimeException; @@ -14,7 +15,7 @@ use stdClass; /** * Unit tests for the ViewFactory class. */ -class PlaylistArchiveStreamTest extends TestCase +class PlaylistArchiveStreamTest extends BaseTest { /** * PlaylistArchiveStream instance. @@ -28,19 +29,11 @@ class PlaylistArchiveStreamTest extends TestCase */ protected function setUp() { - if (PHP_OS == 'WINNT') { - $configFile = 'config_test_windows.yml'; - } else { - $configFile = 'config_test.yml'; - } + parent::setUp(); - $entry = new stdClass(); - $entry->url = 'BaW_jenozKc'; + $video = new Video('https://www.youtube.com/playlist?list=PL1j4Ff8cAqPu5iowaeUAY8lRgkfT4RybJ'); - $video = new stdClass(); - $video->entries = [$entry, $entry]; - - $this->stream = new PlaylistArchiveStream(Config::getInstance('config/'.$configFile), $video, 'worst'); + $this->stream = new PlaylistArchiveStream($video); } /** @@ -57,11 +50,10 @@ class PlaylistArchiveStreamTest extends TestCase * Test the write() function. * * @return void - * @expectedException RuntimeException */ public function testWrite() { - $this->stream->write('foo'); + $this->assertNull($this->stream->write('foo')); } /** @@ -78,11 +70,12 @@ class PlaylistArchiveStreamTest extends TestCase * Test the seek() function. * * @return void - * @expectedException RuntimeException */ public function testSeek() { - $this->stream->seek(42); + $this->stream->write('foobar'); + $this->stream->seek(3); + $this->assertEquals(3, $this->stream->tell()); } /** @@ -92,13 +85,9 @@ class PlaylistArchiveStreamTest extends TestCase */ public function testRead() { - while (!$this->stream->eof()) { - $result = $this->stream->read(8192); - $this->assertInternalType('string', $result); - if (is_string($result)) { - $this->assertLessThanOrEqual(8192, strlen($result)); - } - } + $result = $this->stream->read(8192); + $this->assertInternalType('string', $result); + $this->assertLessThanOrEqual(8192, strlen($result)); } /** @@ -128,18 +117,18 @@ class PlaylistArchiveStreamTest extends TestCase */ public function testIsSeekable() { - $this->assertFalse($this->stream->isSeekable()); + $this->assertTrue($this->stream->isSeekable()); } /** * Test the rewind() function. * * @return void - * @expectedException RuntimeException */ public function testRewind() { $this->stream->rewind(); + $this->assertEquals(0, $this->stream->tell()); } /** @@ -149,7 +138,7 @@ class PlaylistArchiveStreamTest extends TestCase */ public function testIsWritable() { - $this->assertFalse($this->stream->isWritable()); + $this->assertTrue($this->stream->isWritable()); } /** @@ -179,7 +168,7 @@ class PlaylistArchiveStreamTest extends TestCase */ public function testGetMetadata() { - $this->assertNull($this->stream->getMetadata()); + $this->assertInternalType('array', $this->stream->getMetadata()); } /** diff --git a/tests/UglyRouterTest.php b/tests/UglyRouterTest.php index 541b78d..fb26a61 100644 --- a/tests/UglyRouterTest.php +++ b/tests/UglyRouterTest.php @@ -13,7 +13,7 @@ use Slim\Http\Request; /** * Unit tests for the UglyRouter class. */ -class UglyRouterTest extends TestCase +class UglyRouterTest extends BaseTest { /** * UglyRouter instance. diff --git a/tests/VideoDownloadStubsTest.php b/tests/VideoStubsTest.php similarity index 59% rename from tests/VideoDownloadStubsTest.php rename to tests/VideoStubsTest.php index c97f5f0..07ca0fe 100644 --- a/tests/VideoDownloadStubsTest.php +++ b/tests/VideoStubsTest.php @@ -1,59 +1,40 @@ config = Config::getInstance('config/'.$configFile); - $this->download = new VideoDownload($this->config); - $this->url = 'https://www.youtube.com/watch?v=XJC9_JkzugE'; + $this->video = new Video('https://www.youtube.com/watch?v=XJC9_JkzugE'); } /** @@ -74,7 +55,7 @@ class VideoDownloadStubsTest extends TestCase */ public function testGetAudioStreamWithPopenError() { - $this->download->getAudioStream($this->url, 'best'); + $this->video->getAudioStream(); } /** @@ -85,7 +66,7 @@ class VideoDownloadStubsTest extends TestCase */ public function testGetM3uStreamWithPopenError() { - $this->download->getM3uStream($this->download->getJSON($this->url, 'best')); + $this->video->getM3uStream(); } /** @@ -96,7 +77,7 @@ class VideoDownloadStubsTest extends TestCase */ public function testGetRtmpStreamWithPopenError() { - $this->download->getRtmpStream($this->download->getJSON($this->url, 'best')); + $this->video->getRtmpStream(); } /** @@ -107,7 +88,8 @@ class VideoDownloadStubsTest extends TestCase */ public function testGetRemuxStreamWithPopenError() { - $this->download->getRemuxStream([$this->url, $this->url]); + $video = $this->video->withFormat('bestvideo+bestaudio'); + $video->getRemuxStream(); } /** @@ -118,6 +100,6 @@ class VideoDownloadStubsTest extends TestCase */ public function testGetConvertedStreamWithPopenError() { - $this->download->getConvertedStream($this->url, 'best', 32, 'flv'); + $this->video->getConvertedStream(32, 'flv'); } } diff --git a/tests/VideoDownloadTest.php b/tests/VideoTest.php similarity index 68% rename from tests/VideoDownloadTest.php rename to tests/VideoTest.php index a16c15b..64f125f 100644 --- a/tests/VideoDownloadTest.php +++ b/tests/VideoTest.php @@ -1,92 +1,32 @@ config = Config::getInstance('config/'.$configFile); - $this->download = new VideoDownload($this->config); - } - - /** - * Destroy properties after test. - */ - protected function tearDown() - { - Config::destroyInstance(); - } - - /** - * Test VideoDownload constructor with wrong youtube-dl path. - * - * @return void - * @expectedException Exception - */ - public function testConstructorWithMissingYoutubedl() - { - $this->config->youtubedl = 'foo'; - new VideoDownload($this->config); - } - - /** - * Test VideoDownload constructor with wrong Python path. - * - * @return void - * @expectedException Exception - */ - public function testConstructorWithMissingPython() - { - $this->config->python = 'foo'; - new VideoDownload($this->config); - } - - /** - * Test listExtractors function. + * Test getExtractors function. * * @return void */ - public function testListExtractors() + public function testGetExtractors() { - $extractors = $this->download->listExtractors(); - $this->assertContains('youtube', $extractors); + $this->assertContains('youtube', Video::getExtractors()); } /** - * Test getURL function. + * Test getUrl function. * * @param string $url URL * @param string $format Format @@ -99,61 +39,70 @@ class VideoDownloadTest extends TestCase * @dataProvider m3uUrlProvider * @dataProvider remuxUrlProvider */ - public function testGetURL( + public function testgetUrl( $url, $format, /* @scrutinizer ignore-unused */ $filename, /* @scrutinizer ignore-unused */ $extension, $domain ) { - $videoURL = $this->download->getURL($url, $format); - $this->assertContains($domain, $videoURL[0]); + $video = new Video($url, $format); + foreach ($video->getUrl() as $videoURL) { + $this->assertContains($domain, $videoURL); + } } /** - * Test getURL function with a protected video. + * Test getUrl function with a protected video. * * @return void */ - public function testGetURLWithPassword() + public function testgetUrlWithPassword() { if (getenv('CI')) { $this->markTestSkipped('Travis is blacklisted by Vimeo.'); } - $videoURL = $this->download->getURL('http://vimeo.com/68375962', null, 'youtube-dl'); - $this->assertContains('vimeocdn.com', $videoURL[0]); + + $video = new Video('http://vimeo.com/68375962', 'best', 'youtube-dl'); + foreach ($video->getUrl() as $videoURL) { + $this->assertContains('vimeocdn.com', $videoURL); + } } /** - * Test getURL function with a protected video and no password. + * Test getUrl function with a protected video and no password. * * @return void * @expectedException Alltube\PasswordException */ - public function testGetURLWithMissingPassword() + public function testgetUrlWithMissingPassword() { if (getenv('CI')) { $this->markTestSkipped('Travis is blacklisted by Vimeo.'); } - $this->download->getURL('http://vimeo.com/68375962'); + + $video = new Video('http://vimeo.com/68375962'); + $video->getUrl(); } /** - * Test getURL function with a protected video and a wrong password. + * Test getUrl function with a protected video and a wrong password. * * @return void * @expectedException Exception */ - public function testGetURLWithWrongPassword() + public function testgetUrlWithWrongPassword() { if (getenv('CI')) { $this->markTestSkipped('Travis is blacklisted by Vimeo.'); } - $this->download->getURL('http://vimeo.com/68375962', null, 'foo'); + + $video = new Video('http://vimeo.com/68375962', 'best', 'foo'); + $video->getUrl(); } /** - * Test getURL function errors. + * Test getUrl function errors. * * @param string $url URL * @@ -161,9 +110,10 @@ class VideoDownloadTest extends TestCase * @expectedException Exception * @dataProvider ErrorUrlProvider */ - public function testGetURLError($url) + public function testgetUrlError($url) { - $this->download->getURL($url); + $video = new Video($url); + $video->getUrl(); } /** @@ -298,9 +248,10 @@ class VideoDownloadTest extends TestCase * @dataProvider urlProvider * @dataProvider m3uUrlProvider */ - public function testGetJSON($url, $format) + public function testGetJson($url, $format) { - $info = $this->download->getJSON($url, $format); + $video = new Video($url, $format); + $info = $video->getJson(); $this->assertObjectHasAttribute('webpage_url', $info); $this->assertObjectHasAttribute('url', $info); $this->assertObjectHasAttribute('ext', $info); @@ -318,9 +269,10 @@ class VideoDownloadTest extends TestCase * @expectedException Exception * @dataProvider ErrorURLProvider */ - public function testGetJSONError($url) + public function testGetJsonError($url) { - $this->download->getJSON($url); + $video = new Video($url); + $video->getJson(); } /** @@ -338,8 +290,8 @@ class VideoDownloadTest extends TestCase */ public function testGetFilename($url, $format, $filename, $extension) { - $videoFilename = $this->download->getFilename($url, $format); - $this->assertEquals($videoFilename, $filename.'.'.$extension); + $video = new Video($url, $format); + $this->assertEquals($video->getFilename(), $filename.'.'.$extension); } /** @@ -353,25 +305,8 @@ class VideoDownloadTest extends TestCase */ public function testGetFilenameError($url) { - $this->download->getFilename($url); - } - - /** - * Test getAudioFilename function. - * - * @param string $url URL - * @param string $format Format - * @param string $filename Filename - * - * @return void - * @dataProvider urlProvider - * @dataProvider m3uUrlProvider - * @dataProvider remuxUrlProvider - */ - public function testGetAudioFilename($url, $format, $filename) - { - $videoFilename = $this->download->getAudioFilename($url, $format); - $this->assertEquals($videoFilename, $filename.'.mp3'); + $video = new Video($url); + $video->getFilename(); } /** @@ -385,9 +320,8 @@ class VideoDownloadTest extends TestCase */ public function testGetAudioStream($url, $format) { - $stream = $this->download->getAudioStream($url, $format); - $this->assertInternalType('resource', $stream); - $this->assertFalse(feof($stream)); + $video = new Video($url, $format); + $this->assertStream($video->getAudioStream()); } /** @@ -402,9 +336,10 @@ class VideoDownloadTest extends TestCase */ public function testGetAudioStreamAvconvError($url, $format) { - $this->config->avconv = 'foobar'; - $download = new VideoDownload($this->config); - $download->getAudioStream($url, $format); + Config::setOptions(['avconv' => 'foobar']); + + $video = new Video($url, $format); + $video->getAudioStream(); } /** @@ -419,7 +354,8 @@ class VideoDownloadTest extends TestCase */ public function testGetAudioStreamM3uError($url, $format) { - $this->download->getAudioStream($url, $format); + $video = new Video($url, $format); + $video->getAudioStream(); } /** @@ -430,7 +366,12 @@ class VideoDownloadTest extends TestCase */ public function testGetAudioStreamDashError() { - $this->download->getAudioStream('https://vimeo.com/251997032', 'bestaudio/best'); + if (getenv('CI')) { + $this->markTestSkipped('Travis is blacklisted by Vimeo.'); + } + + $video = new Video('https://vimeo.com/251997032', 'bestaudio/best'); + $video->getAudioStream(); } /** @@ -441,10 +382,11 @@ class VideoDownloadTest extends TestCase */ public function testGetAudioStreamPlaylistError() { - $this->download->getAudioStream( + $video = new Video( 'https://www.youtube.com/playlist?list=PLgdySZU6KUXL_8Jq5aUkyNV7wCa-4wZsC', 'best' ); + $video->getAudioStream(); } /** @@ -471,11 +413,8 @@ class VideoDownloadTest extends TestCase */ public function testGetM3uStream($url, $format) { - $this->assertStream( - $this->download->getM3uStream( - $this->download->getJSON($url, $format) - ) - ); + $video = new Video($url, $format); + $this->assertStream($video->getM3uStream()); } /** @@ -489,10 +428,24 @@ class VideoDownloadTest extends TestCase */ public function testGetRemuxStream($url, $format) { - $urls = $this->download->getURL($url, $format); - if (count($urls) > 1) { - $this->assertStream($this->download->getRemuxStream($urls)); - } + $video = new Video($url, $format); + $this->assertStream($video->getRemuxStream()); + } + + /** + * Test getRemuxStream function with a video with only one URL. + * + * @param string $url URL + * @param string $format Format + * + * @return void + * @dataProvider urlProvider + * @expectedException Exception + */ + public function testGetRemuxStreamWithWrongVideo($url, $format) + { + $video = new Video($url, $format); + $video->getRemuxStream(); } /** @@ -508,11 +461,9 @@ class VideoDownloadTest extends TestCase { $this->markTestIncomplete('We need to find another RTMP video.'); - $this->assertStream( - $this->download->getRtmpStream( - $this->download->getJSON($url, $format) - ) - ); + $video = new Video($url, $format); + + $this->assertStream($video->getRtmpStream()); } /** @@ -527,10 +478,10 @@ class VideoDownloadTest extends TestCase */ public function testGetM3uStreamAvconvError($url, $format) { - $this->config->avconv = 'foobar'; - $download = new VideoDownload($this->config); - $video = $download->getJSON($url, $format); - $download->getM3uStream($video); + Config::setOptions(['avconv' => 'foobar']); + + $video = new Video($url, $format); + $video->getM3uStream(); } /** @@ -544,7 +495,8 @@ class VideoDownloadTest extends TestCase */ public function testGetConvertedStream($url, $format) { - $this->assertStream($this->download->getConvertedStream($url, $format, 32, 'flv')); + $video = new Video($url, $format); + $this->assertStream($video->getConvertedStream(32, 'flv')); } /** @@ -559,6 +511,7 @@ class VideoDownloadTest extends TestCase */ public function testGetConvertedStreamM3uError($url, $format) { - $this->download->getConvertedStream($url, $format, 32, 'flv'); + $video = new Video($url, $format); + $video->getConvertedStream(32, 'flv'); } } diff --git a/tests/ViewFactoryTest.php b/tests/ViewFactoryTest.php index 285340f..df1358c 100644 --- a/tests/ViewFactoryTest.php +++ b/tests/ViewFactoryTest.php @@ -15,7 +15,7 @@ use Slim\Views\Smarty; /** * Unit tests for the ViewFactory class. */ -class ViewFactoryTest extends TestCase +class ViewFactoryTest extends BaseTest { /** * Test the create() function.