
diff --git a/.appveyor.yml b/.appveyor.yml index 817effc..75001db 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -2,16 +2,17 @@ install: - sc config wuauserv start= auto - net start wuauserv - - cinst php composer ffmpeg phantomjs + - cinst php --version 7.1.28 + - cinst composer ffmpeg phantomjs - refreshenv - - copy C:\tools\php72\php.ini-development C:\tools\php72\php.ini - - echo extension=C:\tools\php72\ext\php_gmp.dll >> C:\tools\php72\php.ini - - echo extension=C:\tools\php72\ext\php_gettext.dll >> C:\tools\php72\php.ini - - echo extension=C:\tools\php72\ext\php_intl.dll >> C:\tools\php72\php.ini - - echo extension=C:\tools\php72\ext\php_openssl.dll >> C:\tools\php72\php.ini - - echo extension=C:\tools\php72\ext\php_mbstring.dll >> C:\tools\php72\php.ini + - copy C:\tools\php71\php.ini-development C:\tools\php71\php.ini + - echo extension=C:\tools\php71\ext\php_gmp.dll >> C:\tools\php71\php.ini + - echo extension=C:\tools\php71\ext\php_gettext.dll >> C:\tools\php71\php.ini + - echo extension=C:\tools\php71\ext\php_intl.dll >> C:\tools\php71\php.ini + - echo extension=C:\tools\php71\ext\php_openssl.dll >> C:\tools\php71\php.ini + - echo extension=C:\tools\php71\ext\php_mbstring.dll >> C:\tools\php71\php.ini - composer install --no-dev - - composer global require phpunit/phpunit + - composer global require phpunit/phpunit:^6.0 - C:\Python36\python.exe -m pip install youtube-dl test_script: diff --git a/Gruntfile.js b/Gruntfile.js index 6850a24..86d32e6 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -33,6 +33,19 @@ module.exports = function (grunt) { src: ['tests/*.php'] } }, + phpstan: { + options: { + level: 'max', + bin: 'vendor/bin/phpstan', + config: 'phpstan.neon' + }, + php: { + src: ['*.php', 'classes/*.php', 'controllers/*.php'] + }, + tests: { + src: ['tests/*.php'] + } + }, jslint: { js: { src: ['js/*.js'] @@ -57,7 +70,7 @@ module.exports = function (grunt) { options: { archive: 'alltube-<%= githash.main.tag %>.zip' }, - src: ['*.php', 'config/*', '!config/config.yml', 'dist/**', '.htaccess', 'img/**', 'LICENSE', 'README.md', 'robots.txt', 'resources/sitemap.xml', 'resources/manifest.json', 'templates/**', 'templates_c/', 'vendor/**', 'classes/**', 'controllers/**', 'bower_components/**', 'i18n/**', '!vendor/ffmpeg/**', '!vendor/bin/ffmpeg', '!vendor/anam/phantomjs-linux-x86-binary/**', '!vendor/bin/phantomjs', '!vendor/phpunit/**', '!vendor/squizlabs/**', '!vendor/rinvex/country/resources/geodata/*.json', '!vendor/rinvex/country/resources/flags/*.svg', 'node_modules/open-sans-fontface/fonts/**'] + src: ['*.php', 'config/*', '!config/config.yml', 'dist/**', '.htaccess', 'img/**', 'LICENSE', 'README.md', 'robots.txt', 'resources/sitemap.xml', 'resources/manifest.json', 'templates/**', 'templates_c/', 'vendor/**', 'classes/**', 'controllers/**', 'bower_components/**', 'i18n/**', '!vendor/ffmpeg/**', '!vendor/bin/ffmpeg', '!vendor/anam/phantomjs-linux-x86-binary/**', '!vendor/bin/phantomjs', '!vendor/phpunit/**', '!vendor/squizlabs/**', '!vendor/rinvex/countries/resources/geodata/*.json', '!vendor/countries/country/resources/flags/*.svg', 'node_modules/open-sans-fontface/fonts/**'] } }, phpdocumentor: { @@ -125,9 +138,10 @@ module.exports = function (grunt) { grunt.loadNpmTasks('grunt-potomo'); grunt.loadNpmTasks('grunt-contrib-csslint'); grunt.loadNpmTasks('grunt-markdownlint'); + grunt.loadNpmTasks('grunt-phpstan'); grunt.registerTask('default', ['cssmin', 'potomo']); - grunt.registerTask('lint', ['csslint', 'fixpack', 'jsonlint', 'markdownlint', 'phpcs']); + grunt.registerTask('lint', ['csslint', 'fixpack', 'jsonlint', 'markdownlint', 'phpcs', 'phpstan']); grunt.registerTask('test', ['phpunit']); grunt.registerTask('doc', ['phpdocumentor']); grunt.registerTask('release', ['default', 'githash', 'compress']); 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 cf55404..1e8e9a1 100644 --- a/classes/Config.php +++ b/classes/Config.php @@ -16,7 +16,7 @@ class Config /** * Singleton instance. * - * @var Config + * @var Config|null */ private static $instance; @@ -129,18 +129,50 @@ 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 = []) { - if (isset($options) && is_array($options)) { - foreach ($options as $option => $value) { - if (isset($this->$option) && isset($value)) { - $this->$option = $value; - } + $this->applyOptions($options); + $this->getEnv(); + $this->validateOptions(); + } + + /** + * Throw an exception if some of the options are invalid. + * + * @throws Exception If youtube-dl is missing + * @throws Exception If Python is missing + * + * @return void + */ + 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(); } /** @@ -161,34 +193,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 bool $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/LocaleManager.php b/classes/LocaleManager.php index cbc1ddd..c2bbc14 100644 --- a/classes/LocaleManager.php +++ b/classes/LocaleManager.php @@ -6,7 +6,6 @@ namespace Alltube; use Aura\Session\Segment; -use Aura\Session\SessionFactory; use Symfony\Component\Process\Process; /** @@ -24,7 +23,7 @@ class LocaleManager /** * Current locale. * - * @var Locale + * @var Locale|null */ private $curLocale; @@ -37,13 +36,10 @@ class LocaleManager /** * LocaleManager constructor. - * - * @param array $cookies Cookie array */ - public function __construct(array $cookies = []) + public function __construct() { - $session_factory = new SessionFactory(); - $session = $session_factory->newInstance($cookies); + $session = SessionManager::getSession(); $this->sessionSegment = $session->getSegment(self::class); $cookieLocale = $this->sessionSegment->get('locale'); if (isset($cookieLocale)) { @@ -78,7 +74,7 @@ class LocaleManager /** * Get the current locale. * - * @return Locale + * @return Locale|null */ public function getLocale() { diff --git a/classes/LocaleMiddleware.php b/classes/LocaleMiddleware.php index 51dfe6e..9aeb81e 100644 --- a/classes/LocaleMiddleware.php +++ b/classes/LocaleMiddleware.php @@ -37,7 +37,7 @@ class LocaleMiddleware * * @param array $proposedLocale Locale array created by AcceptLanguage::parse() * - * @return string Locale name if chosen, nothing otherwise + * @return Locale Locale if chosen, nothing otherwise */ public function testLocale(array $proposedLocale) { @@ -65,7 +65,7 @@ class LocaleMiddleware { $headers = $request->getHeader('Accept-Language'); $curLocale = $this->localeManager->getLocale(); - if (!isset($curLocale)) { + if (is_null($curLocale)) { if (isset($headers[0])) { $this->localeManager->setLocale( AcceptLanguage::detect([$this, 'testLocale'], new Locale('en_US'), $headers[0]) diff --git a/classes/PlaylistArchiveStream.php b/classes/PlaylistArchiveStream.php deleted file mode 100644 index 3c45a04..0000000 --- a/classes/PlaylistArchiveStream.php +++ /dev/null @@ -1,209 +0,0 @@ -client = new Client(); - $this->download = new VideoDownload($config); - } - - /** - * Add data to the archive. - * - * @param string $data Data - * - * @return void - */ - protected function send($data) - { - $pos = ftell($this->buffer); - fwrite($this->buffer, $data); - if ($pos !== false) { - fseek($this->buffer, $pos); - } - } - - /** - * Called when fopen() is used on the stream. - * - * @param string $path Playlist path (should be playlist://url1;url2;.../format) - * - * @return bool - */ - public function stream_open($path) - { - $this->format = ltrim(parse_url($path, PHP_URL_PATH), '/'); - $buffer = fopen('php://temp', 'r+'); - if ($buffer !== false) { - $this->buffer = $buffer; - } - foreach (explode(';', parse_url($path, PHP_URL_HOST)) as $url) { - $this->files[] = [ - 'url' => urldecode($url), - 'headersSent' => false, - 'complete' => false, - 'stream' => null, - ]; - } - - return true; - } - - /** - * Called when fwrite() is used on the stream. - * - * @return int - */ - public function stream_write() - { - //We don't support writing to a stream - return 0; - } - - /** - * Called when fstat() is used on the stream. - * - * @return array - */ - public function stream_stat() - { - //We need this so Slim won't try to get the size of the stream - return [ - 'mode' => 0010000, - ]; - } - - /** - * Called when ftell() is used on the stream. - * - * @return int|false - */ - public function stream_tell() - { - return ftell($this->buffer); - } - - /** - * Called when fseek() is used on the stream. - * - * @param int $offset Offset - * - * @return bool - */ - public function stream_seek($offset) - { - return fseek($this->buffer, $offset) == 0; - } - - /** - * Called when feof() is used on the stream. - * - * @return bool - */ - public function stream_eof() - { - foreach ($this->files as $file) { - if (!$file['complete']) { - return false; - } - } - - return true; - } - - /** - * Called when fread() is used on the stream. - * - * @param int $count Number of bytes to read - * - * @return string|false - */ - public function stream_read($count) - { - if (!$this->files[$this->curFile]['headersSent']) { - $urls = $this->download->getUrl($this->files[$this->curFile]['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->files[$this->curFile]['url'], $this->format), - $contentLengthHeaders[0] - ); - - $this->files[$this->curFile]['headersSent'] = true; - $this->files[$this->curFile]['stream'] = $response->getBody(); - } elseif (!$this->files[$this->curFile]['stream']->eof()) { - $this->stream_file_part($this->files[$this->curFile]['stream']->read($count)); - } elseif (!$this->files[$this->curFile]['complete']) { - $this->complete_file_stream(); - $this->files[$this->curFile]['complete'] = true; - } elseif (isset($this->files[$this->curFile])) { - $this->curFile += 1; - } - - return fread($this->buffer, $count); - } -} diff --git a/classes/SessionManager.php b/classes/SessionManager.php new file mode 100644 index 0000000..0ecd586 --- /dev/null +++ b/classes/SessionManager.php @@ -0,0 +1,37 @@ +newInstance($_COOKIE); + } + + return self::$session; + } +} diff --git a/classes/VideoDownload.php b/classes/Video.php similarity index 58% rename from classes/VideoDownload.php rename to classes/Video.php index 0f3c462..127102c 100644 --- a/classes/VideoDownload.php +++ b/classes/Video.php @@ -5,14 +5,30 @@ namespace Alltube; +use Alltube\Exception\EmptyUrlException; +use Alltube\Exception\PasswordException; 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 behaves, this class can also contain information about a playlist. + * + * @property-read string $title Title + * @property-read string $protocol Network protocol (HTTP, RTMP, etc.) + * @property-read string $url File URL + * @property-read string $ext File extension + * @property-read string $extractor_key youtube-dl extractor class used + * @property-read array $entries List of videos (if the object contains information about a playlist) + * @property-read array $rtmp_conn + * @property-read string|null $_type Object type (usually "playlist" or null) + * @property-read stdClass $downloader_options */ -class VideoDownload +class Video { /** * Config instance. @@ -21,30 +37,54 @@ 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|null + */ + private $password; + + /** + * JSON object returned by youtube-dl. + * + * @var stdClass + */ + private $json; + + /** + * URLs of the video files. + * + * @var array + */ + private $urls; + /** * 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(); } /** @@ -54,12 +94,14 @@ class VideoDownload * * @return Process */ - private function getProcess(array $arguments) + private static 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,42 +112,29 @@ 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::callYoutubedl(['--list-extractors']))); } /** - * Get a property from youtube-dl. + * Call youtube-dl. * - * @param string $url URL to parse - * @param string $format Format - * @param string $prop Property - * @param string $password Video password + * @param array $arguments Arguments * * @throws PasswordException If the video is protected by a password and no password was specified * @throws Exception If the password is wrong * @throws Exception If youtube-dl returns an error * - * @return string + * @return string Result */ - private function getProp($url, $format = null, $prop = 'dump-json', $password = null) + private static function callYoutubedl(array $arguments) { - $arguments = [ - '--'.$prop, - $url, - ]; - if (isset($format)) { - $arguments[] = '-f '.$format; - } - if (isset($password)) { - $arguments[] = '--video-password'; - $arguments[] = $password; - } + $config = Config::getInstance(); - $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()) { @@ -123,18 +152,70 @@ class VideoDownload } } + /** + * Get a property from youtube-dl. + * + * @param string $prop Property + * + * @return string + */ + private function getProp($prop = 'dump-json') + { + $arguments = ['--'.$prop]; + + 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; + } + + return $this::callYoutubedl($arguments); + } + /** * 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 object Decoded JSON + * @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 bool + */ + public function __isset($name) + { + return isset($this->getJson()->$name); } /** @@ -144,52 +225,44 @@ 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)); + // Cache the URLs. + if (!isset($this->urls)) { + $this->urls = explode("\n", $this->getProp('get-url')); - if (empty($urls[0])) { - throw new EmptyUrlException(_('youtube-dl returned an empty URL.')); + if (empty($this->urls[0])) { + throw new EmptyUrlException(_('youtube-dl returned an empty URL.')); + } } - return $urls; + return $this->urls; } /** * 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 +270,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 object $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 +288,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 +312,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 +323,6 @@ class VideoDownload /** * Get a process that runs avconv in order to convert a video. * - * @param object $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 +334,6 @@ class VideoDownload * @return Process Process */ private function getAvconvProcess( - stdClass $video, $audioBitrate, $filetype = 'mp3', $audioOnly = true, @@ -312,14 +367,16 @@ class VideoDownload $afterArguments[] = $to; } + $urls = $this->getUrl(); + $arguments = array_merge( [ $this->config->avconv, '-v', $this->config->avconvVerbosity, ], - $this->getRtmpArguments($video), + $this->getRtmpArguments(), [ - '-i', $video->url, + '-i', $urls[0], '-f', $filetype, '-b:a', $audioBitrate.'k', ], @@ -328,11 +385,10 @@ class VideoDownload 'pipe:1', ] ); - if ($video->url != '-') { - //Vimeo needs a correct user-agent - $arguments[] = '-user_agent'; - $arguments[] = $this->getProp(null, null, 'dump-user-agent'); - } + + //Vimeo needs a correct user-agent + $arguments[] = '-user_agent'; + $arguments[] = $this->getProp('dump-user-agent'); return new Process($arguments); } @@ -340,34 +396,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 + * @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,25 +432,25 @@ 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.'.'); } + $urls = $this->getUrl(); + $process = new Process( [ $this->config->avconv, '-v', $this->config->avconvVerbosity, - '-i', $video->url, - '-f', $video->ext, + '-i', $urls[0], + '-f', $this->ext, '-c', 'copy', '-bsf:a', 'aac_adtstoasc', '-movflags', 'frag_keyframe+empty_moov', @@ -418,14 +469,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,24 +506,24 @@ 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() { + $urls = $this->getUrl(); + $process = new Process( array_merge( [ $this->config->avconv, '-v', $this->config->avconvVerbosity, ], - $this->getRtmpArguments($video), + $this->getRtmpArguments(), [ - '-i', $video->url, - '-f', $video->ext, + '-i', $urls[0], + '-f', $this->ext, 'pipe:1', ] ) @@ -481,52 +536,24 @@ class VideoDownload return $stream; } - /** - * Get a Tar stream containing every video in the playlist piped through the server. - * - * @param object $video Video object returned by youtube-dl - * @param string $format Requested format - * - * @throws Exception If the popen stream was not created correctly - * - * @return resource - */ - public function getPlaylistArchiveStream(stdClass $video, $format) - { - $playlistItems = []; - foreach ($video->entries as $entry) { - $playlistItems[] = urlencode($entry->url); - } - $stream = fopen('playlist://'.implode(';', $playlistItems).'/'.$format, 'r'); - if (!is_resource($stream)) { - throw new Exception(_('Could not open fopen stream.')); - } - - return $stream; - } - /** * 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'); @@ -536,4 +563,31 @@ 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 self($this->webpageUrl, $format, $this->password); + } + + /** + * Get a HTTP response containing the video. + * + * @param array $headers HTTP headers of the request + * + * @return Response + */ + public function getHttpResponse(array $headers = []) + { + $client = new Client(); + $urls = $this->getUrl(); + + return $client->request('GET', $urls[0], ['stream' => true, 'headers' => $headers]); + } } diff --git a/classes/EmptyUrlException.php b/classes/exceptions/EmptyUrlException.php similarity index 85% rename from classes/EmptyUrlException.php rename to classes/exceptions/EmptyUrlException.php index 663501c..232559a 100644 --- a/classes/EmptyUrlException.php +++ b/classes/exceptions/EmptyUrlException.php @@ -3,7 +3,7 @@ * EmptyUrlException class. */ -namespace Alltube; +namespace Alltube\Exception; use Exception; diff --git a/classes/PasswordException.php b/classes/exceptions/PasswordException.php similarity index 85% rename from classes/PasswordException.php rename to classes/exceptions/PasswordException.php index 598ca92..256d9c8 100644 --- a/classes/PasswordException.php +++ b/classes/exceptions/PasswordException.php @@ -3,7 +3,7 @@ * PasswordException class. */ -namespace Alltube; +namespace Alltube\Exception; use Exception; diff --git a/classes/streams/ConvertedPlaylistArchiveStream.php b/classes/streams/ConvertedPlaylistArchiveStream.php new file mode 100644 index 0000000..b3b1839 --- /dev/null +++ b/classes/streams/ConvertedPlaylistArchiveStream.php @@ -0,0 +1,33 @@ +curVideoStream = new Stream($video->getAudioStream()); + + $this->init_file_stream_transfer( + $video->getFileNameWithExtension('mp3'), + // The ZIP format does not care about the file size. + 0 + ); + } +} diff --git a/classes/streams/PlaylistArchiveStream.php b/classes/streams/PlaylistArchiveStream.php new file mode 100644 index 0000000..7173c0b --- /dev/null +++ b/classes/streams/PlaylistArchiveStream.php @@ -0,0 +1,304 @@ +buffer = $buffer; + } + foreach ($video->entries as $entry) { + $this->videos[] = new Video($entry->url); + } + } + + /** + * Add data to the archive. + * + * @param string $data Data + * + * @return void + */ + protected function send($data) + { + $pos = $this->tell(); + + // 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. + $this->seek($pos); + } + } + + /** + * Write data to the stream. + * + * @param string $string The string that is to be written + * + * @return int + */ + public function write($string) + { + fwrite($this->buffer, $string); + } + + /** + * Get the size of the stream if known. + * + * @return null + */ + public function getSize() + { + } + + /** + * Returns whether or not the stream is seekable. + * + * @return bool + */ + public function isSeekable() + { + return true; + } + + /** + * Seek to the beginning of the stream. + * + * @return void + */ + public function rewind() + { + rewind($this->buffer); + } + + /** + * Returns whether or not the stream is writable. + * + * @return bool + */ + public function isWritable() + { + return true; + } + + /** + * Returns whether or not the stream is readable. + * + * @return bool + */ + public function isReadable() + { + return true; + } + + /** + * Returns the remaining contents in a string. + * + * @return string + */ + public function getContents() + { + return stream_get_contents($this->buffer); + } + + /** + * Get stream metadata as an associative array or retrieve a specific key. + * + * @param string $key string $key Specific metadata to retrieve. + * + * @return array|mixed|null + */ + public function getMetadata($key = null) + { + $meta = stream_get_meta_data($this->buffer); + + if (!isset($key)) { + return $meta; + } + + if (isset($meta[$key])) { + return $meta[$key]; + } + } + + /** + * Separates any underlying resources from the stream. + * + * @return resource + */ + public function detach() + { + $stream = $this->buffer; + $this->close(); + + return $stream; + } + + /** + * Reads all data from the stream into a string, from the beginning to end. + * + * @return string + */ + public function __toString() + { + $this->rewind(); + + return $this->getContents(); + } + + /** + * Returns the current position of the file read/write pointer. + * + * @return int|false + */ + public function tell() + { + return ftell($this->buffer); + } + + /** + * Seek to a position in the stream. + * + * @param int $offset Offset + * @param int $whence Specifies how the cursor position will be calculated + * + * @return void + */ + public function seek($offset, $whence = SEEK_SET) + { + fseek($this->buffer, $offset, $whence); + } + + /** + * Returns true if the stream is at the end of the archive. + * + * @return bool + */ + public function eof() + { + return $this->isComplete && feof($this->buffer); + } + + /** + * Start streaming a new video. + * + * @param Video $video Video to stream + * + * @return void + */ + protected 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] + ); + } + + /** + * Read data from the stream. + * + * @param int $count Number of bytes to read + * + * @return string|false + */ + public function read($count) + { + // 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(); + + $video = next($this->videos); + if ($video) { + // Start streaming the next video. + $this->startVideoStream($video); + } else { + // No video left. + $this->finish(); + $this->isComplete = true; + } + } else { + // Continue streaming the current video. + $this->stream_file_part($this->curVideoStream->read($count)); + } + } else { + // Start streaming the first video. + $this->startVideoStream(current($this->videos)); + } + } + + return fread($this->buffer, $count); + } + + /** + * Closes the stream and any underlying resources. + * + * @return void + */ + public function close() + { + if (is_resource($this->buffer)) { + fclose($this->buffer); + } + if (isset($this->curVideoStream)) { + $this->curVideoStream->close(); + } + } +} diff --git a/classes/streams/YoutubeChunkStream.php b/classes/streams/YoutubeChunkStream.php new file mode 100644 index 0000000..25c3ea8 --- /dev/null +++ b/classes/streams/YoutubeChunkStream.php @@ -0,0 +1,196 @@ +response = $response; + } + + /** + * Read data from the stream. + * + * @param int $length Read up to $length bytes from the object and return + * + * @return string + */ + public function read($length) + { + $size = $this->response->getHeader('Content-Length')[0]; + if ($size - $this->tell() < $length) { + // Don't try to read further than the end of the stream. + $length = $size - $this->tell(); + } + + return $this->response->getBody()->read($length); + } + + /** + * Reads all data from the stream into a string, from the beginning to end. + */ + public function __toString() + { + return (string) $this->response->getBody(); + } + + /** + * Closes the stream and any underlying resources. + * + * @return mixed + */ + public function close() + { + return $this->response->getBody()->close(); + } + + /** + * Separates any underlying resources from the stream. + * + * @return resource|null + */ + public function detach() + { + return $this->response->getBody()->detach(); + } + + /** + * Get the size of the stream if known. + * + * @return int|null + */ + public function getSize() + { + return $this->response->getBody()->getSize(); + } + + /** + * Returns the current position of the file read/write pointer. + * + * @return int + */ + public function tell() + { + return $this->response->getBody()->tell(); + } + + /** + * Returns true if the stream is at the end of the stream. + * + * @return bool + */ + public function eof() + { + return $this->response->getBody()->eof(); + } + + /** + * Returns whether or not the stream is seekable. + * + * @return bool + */ + public function isSeekable() + { + return $this->response->getBody()->isSeekable(); + } + + /** + * Seek to a position in the stream. + * + * @param int $offset Stream offset + * @param int $whence Specifies how the cursor position will be calculated + * + * @return mixed + */ + public function seek($offset, $whence = SEEK_SET) + { + return $this->response->getBody()->seek($offset, $whence); + } + + /** + * Seek to the beginning of the stream. + * + * @return mixed + */ + public function rewind() + { + return $this->response->getBody()->rewind(); + } + + /** + * Returns whether or not the stream is writable. + * + * @return bool + */ + public function isWritable() + { + return $this->response->getBody()->isWritable(); + } + + /** + * Write data to the stream. + * + * @param string $string The string that is to be written + * + * @return mixed + */ + public function write($string) + { + return $this->response->getBody()->write($string); + } + + /** + * Returns whether or not the stream is readable. + * + * @return bool + */ + public function isReadable() + { + return $this->response->getBody()->isReadable(); + } + + /** + * Returns the remaining contents in a string. + * + * @return string + */ + public function getContents() + { + return $this->response->getBody()->getContents(); + } + + /** + * Get stream metadata as an associative array or retrieve a specific key. + * + * @param string $key Specific metadata to retrieve. + * + * @return array|mixed|null + */ + public function getMetadata($key = null) + { + return $this->response->getBody()->getMetadata($key); + } +} diff --git a/classes/streams/YoutubeStream.php b/classes/streams/YoutubeStream.php new file mode 100644 index 0000000..9c413aa --- /dev/null +++ b/classes/streams/YoutubeStream.php @@ -0,0 +1,40 @@ +getHttpResponse(); + $contentLenghtHeader = $stream->getHeader('Content-Length'); + $rangeStart = 0; + + while ($rangeStart < $contentLenghtHeader[0]) { + $rangeEnd = $rangeStart + $video->downloader_options->http_chunk_size; + if ($rangeEnd > $contentLenghtHeader[0]) { + $rangeEnd = $contentLenghtHeader[0] - 1; + } + $response = $video->getHttpResponse(['Range' => 'bytes='.$rangeStart.'-'.$rangeEnd]); + $this->addStream(new YoutubeChunkStream($response)); + $rangeStart = $rangeEnd + 1; + } + } +} diff --git a/composer.json b/composer.json index 5ef3ef0..b20218c 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "homepage": "http://alltubedownload.net/", "type": "project", "require": { - "slim/slim": "~3.11.0", + "slim/slim": "~3.12.1", "mathmarques/smarty-view": "~1.1.0", "symfony/yaml": "~3.4.1", "symfony/process": "~3.4.1", @@ -23,9 +23,10 @@ "phpunit/phpunit": "~6.5.2", "doctrine/instantiator": "~1.0.0", "ffmpeg/ffmpeg": "4.0.3", - "rg3/youtube-dl": "2019.01.17", + "rg3/youtube-dl": "2019.04.24", "heroku/heroku-buildpack-php": "*", - "anam/phantomjs-linux-x86-binary": "~2.1.1" + "anam/phantomjs-linux-x86-binary": "~2.1.1", + "phpstan/phpstan": "~0.9.2" }, "extra": { "paas": { @@ -39,10 +40,10 @@ "type": "package", "package": { "name": "rg3/youtube-dl", - "version": "2019.01.17", + "version": "2019.04.24", "dist": { "type": "zip", - "url": "https://github.com/rg3/youtube-dl/archive/2019.01.17.zip" + "url": "https://github.com/rg3/youtube-dl/archive/2019.04.24.zip" } } }, @@ -78,6 +79,8 @@ "autoload": { "psr-4": { "Alltube\\": "classes/", + "Alltube\\Stream\\": "classes/streams/", + "Alltube\\Exception\\": "classes/exceptions/", "Alltube\\Controller\\": "controllers/", "Alltube\\Test\\": "tests/" } diff --git a/composer.lock b/composer.lock index 2ab3bf0..7caa1ab 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "06c7619047b2e62246e52f538b982231", + "content-hash": "443a32df39c89bd4e58525b41a2de772", "packages": [ { "name": "aura/session", @@ -372,16 +372,16 @@ }, { "name": "mathmarques/smarty-view", - "version": "1.1.1", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/mathmarques/Smarty-View.git", - "reference": "c8f8501a0be4c290e1165fcb9e5064952ef6969d" + "reference": "2ab996e79efcc600cc324b6469c1cdbcd189c9fe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mathmarques/Smarty-View/zipball/c8f8501a0be4c290e1165fcb9e5064952ef6969d", - "reference": "c8f8501a0be4c290e1165fcb9e5064952ef6969d", + "url": "https://api.github.com/repos/mathmarques/Smarty-View/zipball/2ab996e79efcc600cc324b6469c1cdbcd189c9fe", + "reference": "2ab996e79efcc600cc324b6469c1cdbcd189c9fe", "shasum": "" }, "require": { @@ -418,20 +418,20 @@ "template", "view" ], - "time": "2016-08-25T19:04:49+00:00" + "time": "2019-03-31T14:42:41+00:00" }, { "name": "mockery/mockery", - "version": "1.2.0", + "version": "1.2.2", "source": { "type": "git", "url": "https://github.com/mockery/mockery.git", - "reference": "100633629bf76d57430b86b7098cd6beb996a35a" + "reference": "0eb0b48c3f07b3b89f5169ce005b7d05b18cf1d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mockery/mockery/zipball/100633629bf76d57430b86b7098cd6beb996a35a", - "reference": "100633629bf76d57430b86b7098cd6beb996a35a", + "url": "https://api.github.com/repos/mockery/mockery/zipball/0eb0b48c3f07b3b89f5169ce005b7d05b18cf1d2", + "reference": "0eb0b48c3f07b3b89f5169ce005b7d05b18cf1d2", "shasum": "" }, "require": { @@ -440,7 +440,7 @@ "php": ">=5.6.0" }, "require-dev": { - "phpunit/phpunit": "~5.7.10|~6.5|~7.0" + "phpunit/phpunit": "~5.7.10|~6.5|~7.0|~8.0" }, "type": "library", "extra": { @@ -483,7 +483,7 @@ "test double", "testing" ], - "time": "2018-10-02T21:52:37+00:00" + "time": "2019-02-13T09:37:52+00:00" }, { "name": "nikic/fast-route", @@ -533,27 +533,27 @@ }, { "name": "php-mock/php-mock", - "version": "2.0.0", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/php-mock/php-mock.git", - "reference": "22d297231118e6fd5b9db087fbe1ef866c2b95d2" + "reference": "e2eea560cb01502148ca895221f0b58806c5a4df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-mock/php-mock/zipball/22d297231118e6fd5b9db087fbe1ef866c2b95d2", - "reference": "22d297231118e6fd5b9db087fbe1ef866c2b95d2", + "url": "https://api.github.com/repos/php-mock/php-mock/zipball/e2eea560cb01502148ca895221f0b58806c5a4df", + "reference": "e2eea560cb01502148ca895221f0b58806c5a4df", "shasum": "" }, "require": { - "php": ">=5.6", + "php": "^5.6 || ^7.0", "phpunit/php-text-template": "^1" }, "replace": { "malkusch/php-mock": "*" }, "require-dev": { - "phpunit/phpunit": "^5.7" + "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.0" }, "suggest": { "php-mock/php-mock-phpunit": "Allows integration into PHPUnit testcase with the trait PHPMock." @@ -565,7 +565,10 @@ "classes/", "tests/" ] - } + }, + "files": [ + "autoload.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -590,7 +593,7 @@ "test", "test double" ], - "time": "2017-02-17T20:52:52+00:00" + "time": "2019-04-05T22:15:19+00:00" }, { "name": "php-mock/php-mock-integration", @@ -1005,16 +1008,16 @@ }, { "name": "slim/slim", - "version": "3.11.0", + "version": "3.12.1", "source": { "type": "git", "url": "https://github.com/slimphp/Slim.git", - "reference": "d378e70431e78ee92ee32ddde61ecc72edf5dc0a" + "reference": "eaee12ef8d0750db62b8c548016d82fb33addb6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slimphp/Slim/zipball/d378e70431e78ee92ee32ddde61ecc72edf5dc0a", - "reference": "d378e70431e78ee92ee32ddde61ecc72edf5dc0a", + "url": "https://api.github.com/repos/slimphp/Slim/zipball/eaee12ef8d0750db62b8c548016d82fb33addb6b", + "reference": "eaee12ef8d0750db62b8c548016d82fb33addb6b", "shasum": "" }, "require": { @@ -1072,7 +1075,7 @@ "micro", "router" ], - "time": "2018-09-16T10:54:21+00:00" + "time": "2019-04-16T16:47:29+00:00" }, { "name": "smarty-gettext/smarty-gettext", @@ -1184,16 +1187,16 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.10.0", + "version": "v1.11.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "e3d826245268269cd66f8326bd8bc066687b4a19" + "reference": "82ebae02209c21113908c229e9883c419720738a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19", - "reference": "e3d826245268269cd66f8326bd8bc066687b4a19", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/82ebae02209c21113908c229e9883c419720738a", + "reference": "82ebae02209c21113908c229e9883c419720738a", "shasum": "" }, "require": { @@ -1205,7 +1208,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.9-dev" + "dev-master": "1.11-dev" } }, "autoload": { @@ -1227,7 +1230,7 @@ }, { "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" + "email": "backendtea@gmail.com" } ], "description": "Symfony polyfill for ctype functions", @@ -1238,20 +1241,20 @@ "polyfill", "portable" ], - "time": "2018-08-06T14:22:27+00:00" + "time": "2019-02-06T07:57:58+00:00" }, { "name": "symfony/process", - "version": "v3.4.21", + "version": "v3.4.26", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "0d41dd7d95ed179aed6a13393b0f4f97bfa2d25c" + "reference": "a9c4dfbf653023b668c282e4e02609d131f4057a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/0d41dd7d95ed179aed6a13393b0f4f97bfa2d25c", - "reference": "0d41dd7d95ed179aed6a13393b0f4f97bfa2d25c", + "url": "https://api.github.com/repos/symfony/process/zipball/a9c4dfbf653023b668c282e4e02609d131f4057a", + "reference": "a9c4dfbf653023b668c282e4e02609d131f4057a", "shasum": "" }, "require": { @@ -1287,20 +1290,20 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2019-01-02T21:24:08+00:00" + "time": "2019-04-08T16:15:54+00:00" }, { "name": "symfony/yaml", - "version": "v3.4.21", + "version": "v3.4.26", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "554a59a1ccbaac238a89b19c8e551a556fd0e2ea" + "reference": "212a27b731e5bfb735679d1ffaac82bd6a1dc996" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/554a59a1ccbaac238a89b19c8e551a556fd0e2ea", - "reference": "554a59a1ccbaac238a89b19c8e551a556fd0e2ea", + "url": "https://api.github.com/repos/symfony/yaml/zipball/212a27b731e5bfb735679d1ffaac82bd6a1dc996", + "reference": "212a27b731e5bfb735679d1ffaac82bd6a1dc996", "shasum": "" }, "require": { @@ -1346,7 +1349,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2019-01-01T13:45:19+00:00" + "time": "2019-03-25T07:48:46+00:00" }, { "name": "zonuexe/http-accept-language", @@ -1492,9 +1495,7 @@ "version": "4.0.3", "dist": { "type": "xz", - "url": "https://www.johnvansickle.com/ffmpeg/old-releases/ffmpeg-4.0.3-64bit-static.tar.xz", - "reference": null, - "shasum": null + "url": "https://www.johnvansickle.com/ffmpeg/old-releases/ffmpeg-4.0.3-64bit-static.tar.xz" }, "bin": [ "ffmpeg" @@ -1503,16 +1504,16 @@ }, { "name": "heroku/heroku-buildpack-php", - "version": "v148", + "version": "v154", "source": { "type": "git", "url": "https://github.com/heroku/heroku-buildpack-php.git", - "reference": "d331bfb9251d8a091d3a0d29e25ffcdb801577e1" + "reference": "2625279c4b3caf5e2937308d07d4357b411d04d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/heroku/heroku-buildpack-php/zipball/d331bfb9251d8a091d3a0d29e25ffcdb801577e1", - "reference": "d331bfb9251d8a091d3a0d29e25ffcdb801577e1", + "url": "https://api.github.com/repos/heroku/heroku-buildpack-php/zipball/2625279c4b3caf5e2937308d07d4357b411d04d0", + "reference": "2625279c4b3caf5e2937308d07d4357b411d04d0", "shasum": "" }, "bin": [ @@ -1543,7 +1544,58 @@ "nginx", "php" ], - "time": "2018-12-20T21:53:40+00:00" + "time": "2019-04-04T22:05:24+00:00" + }, + { + "name": "jean85/pretty-package-versions", + "version": "1.2", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "75c7effcf3f77501d0e0caa75111aff4daa0dd48" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/75c7effcf3f77501d0e0caa75111aff4daa0dd48", + "reference": "75c7effcf3f77501d0e0caa75111aff4daa0dd48", + "shasum": "" + }, + "require": { + "ocramius/package-versions": "^1.2.0", + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A wrapper for ocramius/package-versions to get pretty versions strings", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "time": "2018-06-13T13:22:40+00:00" }, { "name": "myclabs/deep-copy", @@ -1590,6 +1642,583 @@ ], "time": "2017-10-19T19:58:43+00:00" }, + { + "name": "nette/bootstrap", + "version": "v2.4.6", + "source": { + "type": "git", + "url": "https://github.com/nette/bootstrap.git", + "reference": "268816e3f1bb7426c3a4ceec2bd38a036b532543" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/bootstrap/zipball/268816e3f1bb7426c3a4ceec2bd38a036b532543", + "reference": "268816e3f1bb7426c3a4ceec2bd38a036b532543", + "shasum": "" + }, + "require": { + "nette/di": "~2.4.7", + "nette/utils": "~2.4", + "php": ">=5.6.0" + }, + "conflict": { + "nette/nette": "<2.2" + }, + "require-dev": { + "latte/latte": "~2.2", + "nette/application": "~2.3", + "nette/caching": "~2.3", + "nette/database": "~2.3", + "nette/forms": "~2.3", + "nette/http": "~2.4.0", + "nette/mail": "~2.3", + "nette/robot-loader": "^2.4.2 || ^3.0", + "nette/safe-stream": "~2.2", + "nette/security": "~2.3", + "nette/tester": "~2.0", + "tracy/tracy": "^2.4.1" + }, + "suggest": { + "nette/robot-loader": "to use Configurator::createRobotLoader()", + "tracy/tracy": "to use Configurator::enableTracy()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "? Nette Bootstrap: the simple way to configure and bootstrap your Nette application.", + "homepage": "https://nette.org", + "keywords": [ + "bootstrapping", + "configurator", + "nette" + ], + "time": "2018-05-17T12:52:20+00:00" + }, + { + "name": "nette/di", + "version": "v2.4.15", + "source": { + "type": "git", + "url": "https://github.com/nette/di.git", + "reference": "d0561b8f77e8ef2ed6d83328860e16c81a5a8649" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/di/zipball/d0561b8f77e8ef2ed6d83328860e16c81a5a8649", + "reference": "d0561b8f77e8ef2ed6d83328860e16c81a5a8649", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "nette/neon": "^2.3.3 || ~3.0.0", + "nette/php-generator": "^2.6.1 || ^3.0.0", + "nette/utils": "^2.5.0 || ~3.0.0", + "php": ">=5.6.0" + }, + "conflict": { + "nette/bootstrap": "<2.4", + "nette/nette": "<2.2" + }, + "require-dev": { + "nette/tester": "^2.0", + "tracy/tracy": "^2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "? Nette Dependency Injection Container: Flexible, compiled and full-featured DIC with perfectly usable autowiring and support for all new PHP 7.1 features.", + "homepage": "https://nette.org", + "keywords": [ + "compiled", + "di", + "dic", + "factory", + "ioc", + "nette", + "static" + ], + "time": "2019-01-30T13:26:05+00:00" + }, + { + "name": "nette/finder", + "version": "v2.4.2", + "source": { + "type": "git", + "url": "https://github.com/nette/finder.git", + "reference": "ee951a656cb8ac622e5dd33474a01fd2470505a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/finder/zipball/ee951a656cb8ac622e5dd33474a01fd2470505a0", + "reference": "ee951a656cb8ac622e5dd33474a01fd2470505a0", + "shasum": "" + }, + "require": { + "nette/utils": "~2.4", + "php": ">=5.6.0" + }, + "conflict": { + "nette/nette": "<2.2" + }, + "require-dev": { + "nette/tester": "~2.0", + "tracy/tracy": "^2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "? Nette Finder: find files and directories with an intuitive API.", + "homepage": "https://nette.org", + "keywords": [ + "filesystem", + "glob", + "iterator", + "nette" + ], + "time": "2018-06-28T11:49:23+00:00" + }, + { + "name": "nette/neon", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/nette/neon.git", + "reference": "cbff32059cbdd8720deccf9e9eace6ee516f02eb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/neon/zipball/cbff32059cbdd8720deccf9e9eace6ee516f02eb", + "reference": "cbff32059cbdd8720deccf9e9eace6ee516f02eb", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "ext-json": "*", + "php": ">=7.0" + }, + "require-dev": { + "nette/tester": "^2.0", + "tracy/tracy": "^2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "? Nette NEON: encodes and decodes NEON file format.", + "homepage": "http://ne-on.org", + "keywords": [ + "export", + "import", + "neon", + "nette", + "yaml" + ], + "time": "2019-02-05T21:30:40+00:00" + }, + { + "name": "nette/php-generator", + "version": "v3.0.5", + "source": { + "type": "git", + "url": "https://github.com/nette/php-generator.git", + "reference": "ea90209c2e8a7cd087b2742ca553c047a8df5eff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/php-generator/zipball/ea90209c2e8a7cd087b2742ca553c047a8df5eff", + "reference": "ea90209c2e8a7cd087b2742ca553c047a8df5eff", + "shasum": "" + }, + "require": { + "nette/utils": "^2.4.2 || ~3.0.0", + "php": ">=7.0" + }, + "conflict": { + "nette/nette": "<2.2" + }, + "require-dev": { + "nette/tester": "^2.0", + "tracy/tracy": "^2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "? Nette PHP Generator: generates neat PHP code for you. Supports new PHP 7.2 features.", + "homepage": "https://nette.org", + "keywords": [ + "code", + "nette", + "php", + "scaffolding" + ], + "time": "2018-08-09T14:32:27+00:00" + }, + { + "name": "nette/robot-loader", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/nette/robot-loader.git", + "reference": "3e8d75d6d976e191bdf46752ca40a286671219d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/robot-loader/zipball/3e8d75d6d976e191bdf46752ca40a286671219d2", + "reference": "3e8d75d6d976e191bdf46752ca40a286671219d2", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "nette/finder": "^2.3 || ^3.0", + "nette/utils": "^2.4 || ^3.0", + "php": ">=5.6.0" + }, + "conflict": { + "nette/nette": "<2.2" + }, + "require-dev": { + "nette/tester": "^2.0", + "tracy/tracy": "^2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "? Nette RobotLoader: high performance and comfortable autoloader that will search and autoload classes within your application.", + "homepage": "https://nette.org", + "keywords": [ + "autoload", + "class", + "interface", + "nette", + "trait" + ], + "time": "2019-03-01T20:23:02+00:00" + }, + { + "name": "nette/utils", + "version": "v2.5.3", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "17b9f76f2abd0c943adfb556e56f2165460b15ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/17b9f76f2abd0c943adfb556e56f2165460b15ce", + "reference": "17b9f76f2abd0c943adfb556e56f2165460b15ce", + "shasum": "" + }, + "require": { + "php": ">=5.6.0" + }, + "conflict": { + "nette/nette": "<2.2" + }, + "require-dev": { + "nette/tester": "~2.0", + "tracy/tracy": "^2.3" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize() and toAscii()", + "ext-intl": "for script transliteration in Strings::webalize() and toAscii()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-xml": "to use Strings::length() etc. when mbstring is not available" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.5-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ], + "files": [ + "src/loader.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "? Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "time": "2018-09-18T10:22:16+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v3.1.5", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "bb87e28e7d7b8d9a7fda231d37457c9210faf6ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/bb87e28e7d7b8d9a7fda231d37457c9210faf6ce", + "reference": "bb87e28e7d7b8d9a7fda231d37457c9210faf6ce", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "~4.0|~5.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "time": "2018-02-28T20:30:58+00:00" + }, + { + "name": "ocramius/package-versions", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/Ocramius/PackageVersions.git", + "reference": "ad8a245decad4897cc6b432743913dad0d69753c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Ocramius/PackageVersions/zipball/ad8a245decad4897cc6b432743913dad0d69753c", + "reference": "ad8a245decad4897cc6b432743913dad0d69753c", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0", + "php": "~7.0" + }, + "require-dev": { + "composer/composer": "^1.3", + "ext-zip": "*", + "humbug/humbug": "dev-master", + "phpunit/phpunit": "^6.4" + }, + "type": "composer-plugin", + "extra": { + "class": "PackageVersions\\Installer", + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "PackageVersions\\": "src/PackageVersions" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)", + "time": "2017-11-24T11:07:03+00:00" + }, { "name": "phar-io/manifest", "version": "1.0.1", @@ -1907,6 +2536,114 @@ ], "time": "2018-08-05T17:53:17+00:00" }, + { + "name": "phpstan/phpdoc-parser", + "version": "0.2", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "02f909f134fe06f0cd4790d8627ee24efbe84d6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/02f909f134fe06f0cd4790d8627ee24efbe84d6a", + "reference": "02f909f134fe06f0cd4790d8627ee24efbe84d6a", + "shasum": "" + }, + "require": { + "php": "~7.0" + }, + "require-dev": { + "consistence/coding-standard": "^2.0.0", + "jakub-onderka/php-parallel-lint": "^0.9.2", + "phing/phing": "^2.16.0", + "phpstan/phpstan": "^0.9", + "phpunit/phpunit": "^6.3", + "slevomat/coding-standard": "^3.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.1-dev" + } + }, + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "time": "2018-01-13T18:19:41+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "0.9.2", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "e59541bcc7cac9b35ca54db6365bf377baf4a488" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e59541bcc7cac9b35ca54db6365bf377baf4a488", + "reference": "e59541bcc7cac9b35ca54db6365bf377baf4a488", + "shasum": "" + }, + "require": { + "jean85/pretty-package-versions": "^1.0.3", + "nette/bootstrap": "^2.4 || ^3.0", + "nette/di": "^2.4.7 || ^3.0", + "nette/robot-loader": "^3.0.1", + "nette/utils": "^2.4.5 || ^3.0", + "nikic/php-parser": "^3.1", + "php": "~7.0", + "phpstan/phpdoc-parser": "^0.2", + "symfony/console": "~3.2 || ~4.0", + "symfony/finder": "~3.2 || ~4.0" + }, + "require-dev": { + "consistence/coding-standard": "2.2.1", + "ext-gd": "*", + "ext-intl": "*", + "ext-mysqli": "*", + "jakub-onderka/php-parallel-lint": "^0.9.2", + "phing/phing": "^2.16.0", + "phpstan/phpstan-php-parser": "^0.9", + "phpstan/phpstan-phpunit": "^0.9.3", + "phpstan/phpstan-strict-rules": "^0.9", + "phpunit/phpunit": "^6.5.4", + "slevomat/coding-standard": "4.0.0" + }, + "bin": [ + "bin/phpstan" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.9-dev" + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": [ + "src/", + "build/PHPStan" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "time": "2018-01-28T13:22:19+00:00" + }, { "name": "phpunit/php-code-coverage", "version": "5.3.2", @@ -2117,16 +2854,16 @@ }, { "name": "phpunit/phpunit", - "version": "6.5.13", + "version": "6.5.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "0973426fb012359b2f18d3bd1e90ef1172839693" + "reference": "bac23fe7ff13dbdb461481f706f0e9fe746334b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0973426fb012359b2f18d3bd1e90ef1172839693", - "reference": "0973426fb012359b2f18d3bd1e90ef1172839693", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/bac23fe7ff13dbdb461481f706f0e9fe746334b7", + "reference": "bac23fe7ff13dbdb461481f706f0e9fe746334b7", "shasum": "" }, "require": { @@ -2197,7 +2934,7 @@ "testing", "xunit" ], - "time": "2018-09-08T15:10:43+00:00" + "time": "2019-02-01T05:22:47+00:00" }, { "name": "phpunit/phpunit-mock-objects", @@ -2256,16 +2993,62 @@ "mock", "xunit" ], + "abandoned": true, "time": "2018-08-09T05:50:03+00:00" }, { - "name": "rg3/youtube-dl", - "version": "2019.01.17", + "name": "psr/log", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd" + }, "dist": { "type": "zip", - "url": "https://github.com/rg3/youtube-dl/archive/2019.01.17.zip", - "reference": null, - "shasum": null + "url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", + "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2018-11-20T15:27:04+00:00" + }, + { + "name": "rg3/youtube-dl", + "version": "2019.04.24", + "dist": { + "type": "zip", + "url": "https://github.com/rg3/youtube-dl/archive/2019.04.24.zip" }, "type": "library" }, @@ -2830,16 +3613,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.4.0", + "version": "3.4.2", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "379deb987e26c7cd103a7b387aea178baec96e48" + "reference": "b8a7362af1cc1aadb5bd36c3defc4dda2cf5f0a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/379deb987e26c7cd103a7b387aea178baec96e48", - "reference": "379deb987e26c7cd103a7b387aea178baec96e48", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/b8a7362af1cc1aadb5bd36c3defc4dda2cf5f0a8", + "reference": "b8a7362af1cc1aadb5bd36c3defc4dda2cf5f0a8", "shasum": "" }, "require": { @@ -2872,25 +3655,202 @@ } ], "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", - "homepage": "http://www.squizlabs.com/php-codesniffer", + "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", "keywords": [ "phpcs", "standards" ], - "time": "2018-12-19T23:57:18+00:00" + "time": "2019-04-10T23:49:02+00:00" }, { - "name": "symfony/polyfill-mbstring", - "version": "v1.10.0", + "name": "symfony/console", + "version": "v3.4.26", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "c79c051f5b3a46be09205c73b80b346e4153e494" + "url": "https://github.com/symfony/console.git", + "reference": "15a9104356436cb26e08adab97706654799d31d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/c79c051f5b3a46be09205c73b80b346e4153e494", - "reference": "c79c051f5b3a46be09205c73b80b346e4153e494", + "url": "https://api.github.com/repos/symfony/console/zipball/15a9104356436cb26e08adab97706654799d31d8", + "reference": "15a9104356436cb26e08adab97706654799d31d8", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/debug": "~2.8|~3.0|~4.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/dependency-injection": "<3.4", + "symfony/process": "<3.3" + }, + "provide": { + "psr/log-implementation": "1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~3.3|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/event-dispatcher": "~2.8|~3.0|~4.0", + "symfony/lock": "~3.4|~4.0", + "symfony/process": "~3.3|~4.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "time": "2019-04-08T09:29:13+00:00" + }, + { + "name": "symfony/debug", + "version": "v3.4.26", + "source": { + "type": "git", + "url": "https://github.com/symfony/debug.git", + "reference": "681afbb26488903c5ac15e63734f1d8ac430c9b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/debug/zipball/681afbb26488903c5ac15e63734f1d8ac430c9b9", + "reference": "681afbb26488903c5ac15e63734f1d8ac430c9b9", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "psr/log": "~1.0" + }, + "conflict": { + "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" + }, + "require-dev": { + "symfony/http-kernel": "~2.8|~3.0|~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Debug\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Debug Component", + "homepage": "https://symfony.com", + "time": "2019-04-11T09:48:14+00:00" + }, + { + "name": "symfony/finder", + "version": "v3.4.26", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "61af5ce0b34b942d414fe8f1b11950d0e9a90e98" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/61af5ce0b34b942d414fe8f1b11950d0e9a90e98", + "reference": "61af5ce0b34b942d414fe8f1b11950d0e9a90e98", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Finder Component", + "homepage": "https://symfony.com", + "time": "2019-04-02T19:54:57+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.11.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "fe5e94c604826c35a32fa832f35bd036b6799609" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fe5e94c604826c35a32fa832f35bd036b6799609", + "reference": "fe5e94c604826c35a32fa832f35bd036b6799609", "shasum": "" }, "require": { @@ -2902,7 +3862,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.9-dev" + "dev-master": "1.11-dev" } }, "autoload": { @@ -2936,20 +3896,20 @@ "portable", "shim" ], - "time": "2018-09-21T13:07:52+00:00" + "time": "2019-02-06T07:57:58+00:00" }, { "name": "symfony/var-dumper", - "version": "v3.4.21", + "version": "v3.4.26", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "a5f39641bb62e8b74e343467b145331273f615a2" + "reference": "f0883812642a6d6583a9e2ae6aec4ba134436f40" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/a5f39641bb62e8b74e343467b145331273f615a2", - "reference": "a5f39641bb62e8b74e343467b145331273f615a2", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/f0883812642a6d6583a9e2ae6aec4ba134436f40", + "reference": "f0883812642a6d6583a9e2ae6aec4ba134436f40", "shasum": "" }, "require": { @@ -3005,20 +3965,20 @@ "debug", "dump" ], - "time": "2019-01-01T13:45:19+00:00" + "time": "2019-04-16T13:58:17+00:00" }, { "name": "theseer/tokenizer", - "version": "1.1.0", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b" + "reference": "1c42705be2b6c1de5904f8afacef5895cab44bf8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/cb2f008f3f05af2893a87208fe6a6c4985483f8b", - "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/1c42705be2b6c1de5904f8afacef5895cab44bf8", + "reference": "1c42705be2b6c1de5904f8afacef5895cab44bf8", "shasum": "" }, "require": { @@ -3045,7 +4005,7 @@ } ], "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", - "time": "2017-04-07T12:08:54+00:00" + "time": "2019-04-04T09:56:43+00:00" }, { "name": "webmozart/assert", diff --git a/controllers/BaseController.php b/controllers/BaseController.php new file mode 100644 index 0000000..f96842e --- /dev/null +++ b/controllers/BaseController.php @@ -0,0 +1,109 @@ +config = Config::getInstance(); + $this->container = $container; + $session = SessionManager::getSession(); + $this->sessionSegment = $session->getSegment(self::class); + + if ($this->config->stream) { + $this->defaultFormat = 'best'; + } + } + + /** + * Get video format from request parameters or default format if none is specified. + * + * @param Request $request PSR-7 request + * + * @return string format + */ + protected function getFormat(Request $request) + { + $format = $request->getQueryParam('format'); + if (!isset($format)) { + $format = $this->defaultFormat; + } + + return $format; + } + + /** + * Get the password entered for the current video. + * + * @param Request $request PSR-7 request + * + * @return string Password + */ + protected function getPassword(Request $request) + { + $url = $request->getQueryParam('url'); + + $password = $request->getParam('password'); + if (isset($password)) { + $this->sessionSegment->setFlash($url, $password); + } else { + $password = $this->sessionSegment->getFlash($url); + } + + return $password; + } +} diff --git a/controllers/DownloadController.php b/controllers/DownloadController.php new file mode 100644 index 0000000..ddd83b3 --- /dev/null +++ b/controllers/DownloadController.php @@ -0,0 +1,281 @@ +getQueryParam('url'); + + if (isset($url)) { + $this->video = new Video($url, $this->getFormat($request), $this->getPassword($request)); + + try { + if ($this->config->convert && $request->getQueryParam('audio')) { + // Audio convert. + return $this->getAudioResponse($request, $response); + } elseif ($this->config->convertAdvanced && !is_null($request->getQueryParam('customConvert'))) { + // Advance convert. + return $this->getConvertedResponse($request, $response); + } + + // Regular download. + return $this->getDownloadResponse($request, $response); + } catch (PasswordException $e) { + return $response->withRedirect( + $this->container->get('router')->pathFor('info').'?'.http_build_query($request->getQueryParams()) + ); + } catch (Exception $e) { + $response->getBody()->write($e->getMessage()); + + return $response->withHeader('Content-Type', 'text/plain')->withStatus(500); + } + } else { + return $response->withRedirect($this->container->get('router')->pathFor('index')); + } + } + + /** + * Return a converted MP3 file. + * + * @param Request $request PSR-7 request + * @param Response $response PSR-7 response + * + * @return Response HTTP response + */ + private function getConvertedAudioResponse(Request $request, Response $response) + { + $from = $request->getQueryParam('from'); + $to = $request->getQueryParam('to'); + + $response = $response->withHeader( + 'Content-Disposition', + 'attachment; filename="'. + $this->video->getFileNameWithExtension('mp3').'"' + ); + $response = $response->withHeader('Content-Type', 'audio/mpeg'); + + if ($request->isGet() || $request->isPost()) { + try { + $process = $this->video->getAudioStream($from, $to); + } catch (Exception $e) { + // Fallback to default format. + $this->video = $this->video->withFormat($this->defaultFormat); + $process = $this->video->getAudioStream($from, $to); + } + $response = $response->withBody(new Stream($process)); + } + + return $response; + } + + /** + * Return the MP3 file. + * + * @param Request $request PSR-7 request + * @param Response $response PSR-7 response + * + * @return Response HTTP response + */ + private function getAudioResponse(Request $request, Response $response) + { + try { + // 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) { + $this->video = $this->video->withFormat('mp3'); + + return $this->getStream($request, $response); + } else { + $this->video = $this->video->withFormat('mp3[protocol=https]/mp3[protocol=http]'); + + $urls = $this->video->getUrl(); + + return $response->withRedirect($urls[0]); + } + } catch (PasswordException $e) { + $frontController = new FrontController($this->container); + + return $frontController->password($request, $response); + } catch (Exception $e) { + // If MP3 is not available, we convert it. + $this->video = $this->video->withFormat($this->defaultFormat); + + return $this->getConvertedAudioResponse($request, $response); + } + } + + /** + * Get a video/audio stream piped through the server. + * + * @param Response $response PSR-7 response + * @param Request $request PSR-7 request + * + * @return Response HTTP response + */ + private function getStream(Request $request, Response $response) + { + if (isset($this->video->entries)) { + if ($this->config->convert && $request->getQueryParam('audio')) { + $stream = new ConvertedPlaylistArchiveStream($this->video); + } else { + $stream = new PlaylistArchiveStream($this->video); + } + $response = $response->withHeader('Content-Type', 'application/zip'); + $response = $response->withHeader( + 'Content-Disposition', + 'attachment; filename="'.$this->video->title.'.zip"' + ); + + return $response->withBody($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 { + $stream = $this->video->getHttpResponse(['Range' => $request->getHeader('Range')]); + + $response = $response->withHeader('Content-Type', $stream->getHeader('Content-Type')); + $response = $response->withHeader('Content-Length', $stream->getHeader('Content-Length')); + $response = $response->withHeader('Accept-Ranges', $stream->getHeader('Accept-Ranges')); + $response = $response->withHeader('Content-Range', $stream->getHeader('Content-Range')); + if ($stream->getStatusCode() == 206) { + $response = $response->withStatus(206); + } + + if (isset($this->video->downloader_options->http_chunk_size)) { + // Workaround for Youtube throttling the download speed. + $body = new YoutubeStream($this->video); + } else { + $body = $stream->getBody(); + } + } + if ($request->isGet()) { + $response = $response->withBody($body); + } + $response = $response->withHeader( + 'Content-Disposition', + 'attachment; filename="'. + $this->video->getFilename().'"' + ); + + return $response; + } + + /** + * Get a remuxed stream piped through the server. + * + * @param Response $response PSR-7 response + * @param Request $request PSR-7 request + * + * @return Response HTTP response + */ + 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->video->getRemuxStream(); + $response = $response->withHeader('Content-Type', 'video/x-matroska'); + if ($request->isGet()) { + $response = $response->withBody(new Stream($stream)); + } + + return $response->withHeader( + 'Content-Disposition', + 'attachment; filename="'.$this->video->getFileNameWithExtension('mkv') + ); + } + + /** + * Get approriate HTTP response to download query. + * Depends on whether we want to stream, remux or simply redirect. + * + * @param Response $response PSR-7 response + * @param Request $request PSR-7 request + * + * @return Response HTTP response + */ + private function getDownloadResponse(Request $request, Response $response) + { + try { + $videoUrls = $this->video->getUrl(); + } catch (EmptyUrlException $e) { + /* + If this happens it is probably a playlist + so it will either be handled by getStream() or throw an exception anyway. + */ + $videoUrls = []; + } + if (count($videoUrls) > 1) { + return $this->getRemuxStream($request, $response); + } elseif ($this->config->stream && (isset($this->video->entries) || $request->getQueryParam('stream'))) { + return $this->getStream($request, $response); + } else { + if (empty($videoUrls[0])) { + throw new Exception(_("Can't find URL of video.")); + } + + return $response->withRedirect($videoUrls[0]); + } + } + + /** + * Return a converted video file. + * + * @param Request $request PSR-7 request + * @param Response $response PSR-7 response + * + * @return Response HTTP response + */ + private function getConvertedResponse(Request $request, Response $response) + { + $response = $response->withHeader( + 'Content-Disposition', + 'attachment; filename="'. + $this->video->getFileNameWithExtension($request->getQueryParam('customFormat')).'"' + ); + $response = $response->withHeader('Content-Type', 'video/'.$request->getQueryParam('customFormat')); + + if ($request->isGet() || $request->isPost()) { + $process = $this->video->getConvertedStream( + $request->getQueryParam('customBitrate'), + $request->getQueryParam('customFormat') + ); + $response = $response->withBody(new Stream($process)); + } + + return $response; + } +} diff --git a/controllers/FrontController.php b/controllers/FrontController.php index 7c27d75..c89cad4 100644 --- a/controllers/FrontController.php +++ b/controllers/FrontController.php @@ -6,55 +6,22 @@ namespace Alltube\Controller; use Alltube\Config; -use Alltube\EmptyUrlException; +use Alltube\Exception\PasswordException; use Alltube\Locale; use Alltube\LocaleManager; -use Alltube\PasswordException; -use Alltube\VideoDownload; -use Aura\Session\Segment; -use Aura\Session\SessionFactory; +use Alltube\Video; use Exception; -use GuzzleHttp\Client; use Psr\Container\ContainerInterface; use Slim\Container; use Slim\Http\Request; use Slim\Http\Response; -use Slim\Http\Stream; use Slim\Views\Smarty; /** * Main controller. */ -class FrontController +class FrontController extends BaseController { - /** - * Config instance. - * - * @var Config - */ - private $config; - - /** - * VideoDownload instance. - * - * @var VideoDownload - */ - private $download; - - /** - * Slim dependency container. - * - * @var ContainerInterface - */ - private $container; - - /** - * Session segment used to store session variables. - * - * @var Segment - */ - private $sessionSegment; - /** * Smarty view. * @@ -62,13 +29,6 @@ class FrontController */ private $view; - /** - * Default youtube-dl format. - * - * @var string - */ - private $defaultFormat = 'best[protocol^=http]'; - /** * LocaleManager instance. * @@ -77,31 +37,16 @@ class FrontController private $localeManager; /** - * FrontController constructor. + * BaseController constructor. * - * @param Container $container Slim dependency container - * @param Config $config Config instance - * @param array $cookies Cookie array + * @param ContainerInterface $container Slim dependency container */ - public function __construct(ContainerInterface $container, Config $config = null, array $cookies = []) + public function __construct(ContainerInterface $container) { - if (isset($config)) { - $this->config = $config; - } else { - $this->config = Config::getInstance(); - } - $this->download = new VideoDownload($this->config); - $this->container = $container; - $this->view = $this->container->get('view'); + parent::__construct($container); + $this->localeManager = $this->container->get('locale'); - $session_factory = new SessionFactory(); - $session = $session_factory->newInstance($cookies); - $this->sessionSegment = $session->getSegment(self::class); - if ($this->config->remux) { - $this->defaultFormat = 'bestvideo+bestaudio,best'; - } elseif ($this->config->stream) { - $this->defaultFormat = 'best'; - } + $this->view = $this->container->get('view'); } /** @@ -114,7 +59,7 @@ class FrontController */ public function index(Request $request, Response $response) { - $uri = $request->getUri()->withUserInfo(null); + $uri = $request->getUri()->withUserInfo(''); $this->view->render( $response, 'index.tpl', @@ -163,7 +108,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 '. @@ -202,132 +147,45 @@ class FrontController return $response; } - /** - * Return a converted MP3 file. - * - * @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) - { - if (!isset($params['from'])) { - $params['from'] = ''; - } - if (!isset($params['to'])) { - $params['to'] = ''; - } - - $response = $response->withHeader( - 'Content-Disposition', - 'attachment; filename="'. - $this->download->getAudioFilename($params['url'], 'bestaudio/best', $password).'"' - ); - $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'] - ); - } catch (Exception $e) { - $process = $this->download->getAudioStream( - $params['url'], - $this->defaultFormat, - $password, - $params['from'], - $params['to'] - ); - } - $response = $response->withBody(new Stream($process)); - } - - return $response; - } - - /** - * Return the MP3 file. - * - * @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) - { - try { - if (isset($params['from']) || isset($params['to'])) { - throw new Exception('Force convert when we need to seek.'); - } - - if ($this->config->stream) { - return $this->getStream($params['url'], 'mp3', $response, $request, $password); - } else { - $urls = $this->download->getURL($params['url'], 'mp3[protocol^=http]', $password); - - return $response->withRedirect($urls[0]); - } - } catch (PasswordException $e) { - return $this->password($request, $response); - } catch (Exception $e) { - return $this->getConvertedAudioResponse($request, $response, $params, $password); - } - } - /** * Return the video description page. * * @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 getInfoResponse(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 ($this->config->stream) { - $protocol = ''; - } else { - $protocol = '[protocol^=http]'; - } - if (isset($video->entries)) { + + if (isset($this->video->entries)) { $template = 'playlist.tpl'; } else { - $template = 'video.tpl'; + $template = 'info.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, - 'class' => 'video', - 'title' => $title, - 'description' => $description, - 'protocol' => $protocol, - 'config' => $this->config, - 'canonical' => $this->getCanonicalUrl($request), - 'locale' => $this->localeManager->getLocale(), + 'video' => $this->video, + 'class' => 'info', + 'title' => $title, + 'description' => $description, + 'config' => $this->config, + 'canonical' => $this->getCanonicalUrl($request), + 'locale' => $this->localeManager->getLocale(), + 'defaultFormat' => $this->defaultFormat, ] ); @@ -342,23 +200,21 @@ class FrontController * * @return Response HTTP response */ - public function video(Request $request, Response $response) + public function info(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($url) && !empty($url)) { + $this->video = new Video($url, $this->getFormat($request), $this->getPassword($request)); - if (isset($params['url']) && !empty($params['url'])) { - $password = $request->getParam('password'); - if (isset($password)) { - $this->sessionSegment->setFlash($params['url'], $password); - } - if (isset($params['audio'])) { - return $this->getAudioResponse($request, $response, $params, $password); + if ($this->config->convert && $request->getQueryParam('audio')) { + // We skip the info page and get directly to the download. + return $response->withRedirect( + $this->container->get('router')->pathFor('download'). + '?'.http_build_query($request->getQueryParams()) + ); } else { - return $this->getVideoResponse($request, $response, $params, $password); + return $this->getInfoResponse($request, $response); } } else { return $response->withRedirect($this->container->get('router')->pathFor('index')); @@ -392,265 +248,6 @@ class FrontController return $response->withStatus(500); } - /** - * Get a video/audio stream piped through the server. - * - * @param string $url URL of the video - * @param string $format Requested format - * @param Response $response PSR-7 response - * @param Request $request PSR-7 request - * @param string $password Video password - * - * @return Response HTTP response - */ - private function getStream($url, $format, Response $response, Request $request, $password = null) - { - $video = $this->download->getJSON($url, $format, $password); - if (isset($video->entries)) { - $stream = $this->download->getPlaylistArchiveStream($video, $format); - $response = $response->withHeader('Content-Type', 'application/x-tar'); - $response = $response->withHeader( - 'Content-Disposition', - 'attachment; filename="'.$video->title.'.tar"' - ); - - return $response->withBody(new Stream($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); - } else { - $client = new Client(); - $stream = $client->request( - 'GET', - $video->url, - [ - 'stream' => true, - 'headers' => ['Range' => $request->getHeader('Range')], - ] - ); - $response = $response->withHeader('Content-Type', $stream->getHeader('Content-Type')); - $response = $response->withHeader('Content-Length', $stream->getHeader('Content-Length')); - $response = $response->withHeader('Accept-Ranges', $stream->getHeader('Accept-Ranges')); - $response = $response->withHeader('Content-Range', $stream->getHeader('Content-Range')); - if ($stream->getStatusCode() == 206) { - $response = $response->withStatus(206); - } - $body = $stream->getBody(); - } - if ($request->isGet()) { - $response = $response->withBody($body); - } - $response = $response->withHeader( - 'Content-Disposition', - 'attachment; filename="'. - $this->download->getFilename($url, $format, $password).'"' - ); - - return $response; - } - - /** - * 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) - { - 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="'.$this->download->getFileNameWithExtension( - 'mkv', - $webpageUrl, - $format, - $this->sessionSegment->getFlash($webpageUrl) - ) - ); - } - - /** - * 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) - { - try { - $videoUrls = $this->download->getURL( - $url, - $format, - $this->sessionSegment->getFlash($url) - ); - } catch (EmptyUrlException $e) { - /* - If this happens it is probably a playlist - so it will either be handled by getStream() or throw an exception anyway. - */ - $videoUrls = []; - } - 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 { - if (empty($videoUrls[0])) { - throw new Exception(_("Can't find URL of video.")); - } - - return $response->withRedirect($videoUrls[0]); - } - } - - /** - * Return a converted video file. - * - * @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) - { - $password = $request->getParam('password'); - $response = $response->withHeader( - 'Content-Disposition', - 'attachment; filename="'. - $this->download->getFileNameWithExtension( - $params['customFormat'], - $params['url'], - $format, - $password - ).'"' - ); - $response = $response->withHeader('Content-Type', 'video/'.$params['customFormat']); - - if ($request->isGet() || $request->isPost()) { - $process = $this->download->getConvertedStream( - $params['url'], - $format, - $params['customBitrate'], - $params['customFormat'], - $password - ); - $response = $response->withBody(new Stream($process)); - } - - return $response; - } - - /** - * Redirect to video file. - * - * @param Request $request PSR-7 request - * @param Response $response PSR-7 response - * - * @return Response HTTP response - */ - public function redirect(Request $request, Response $response) - { - $params = $request->getQueryParams(); - $format = $this->getFormat($request); - if (isset($params['url'])) { - try { - if ($this->config->convertAdvanced && !is_null($request->getQueryParam('customConvert'))) { - return $this->getConvertedResponse($request, $response, $params, $format); - } - - return $this->getRedirectResponse($params['url'], $format, $response, $request); - } catch (PasswordException $e) { - return $response->withRedirect( - $this->container->get('router')->pathFor('video').'?url='.urlencode($params['url']) - ); - } catch (Exception $e) { - $response->getBody()->write($e->getMessage()); - - return $response->withHeader('Content-Type', 'text/plain')->withStatus(500); - } - } else { - return $response->withRedirect($this->container->get('router')->pathFor('index')); - } - } - - /** - * Return the JSON object generated by youtube-dl. - * - * @param Request $request PSR-7 request - * @param Response $response PSR-7 response - * - * @return Response HTTP response - */ - public function json(Request $request, Response $response) - { - $params = $request->getQueryParams(); - $format = $this->getFormat($request); - if (isset($params['url'])) { - try { - return $response->withJson( - $this->download->getJSON( - $params['url'], - $format - ) - ); - } catch (Exception $e) { - return $response->withJson(['error' => $e->getMessage()]) - ->withStatus(500); - } - } else { - return $response->withJson(['error' => 'You need to provide the url parameter']) - ->withStatus(400); - } - } - /** * Generate the canonical URL of the current page. * diff --git a/controllers/JsonController.php b/controllers/JsonController.php new file mode 100644 index 0000000..cc36e46 --- /dev/null +++ b/controllers/JsonController.php @@ -0,0 +1,44 @@ +getQueryParam('url'); + + if (isset($url)) { + try { + $this->video = new Video($url, $this->getFormat($request), $this->getPassword($request)); + + return $response->withJson($this->video->getJson()); + } catch (Exception $e) { + return $response->withJson(['error' => $e->getMessage()]) + ->withStatus(500); + } + } else { + return $response->withJson(['error' => 'You need to provide the url parameter']) + ->withStatus(400); + } + } +} diff --git a/css/style.css b/css/style.css index 951ef4d..482ff25 100644 --- a/css/style.css +++ b/css/style.css @@ -1,733 +1,724 @@ - body { - background-color: #EBEBEB; - background-image:url('../img/fond.jpg'); - font-family: 'Open Sans', sans-serif; - font-weight:400; - text-align:center; + background-color: #ebebeb; + background-image: url("../img/fond.jpg"); + font-family: "Open Sans", sans-serif; + font-weight: 400; + text-align: center; } - - /* Header */ header { - padding:0; - position:absolute; - text-align:right; - top:0; - width:100%; + padding: 0; + position: absolute; + text-align: right; + top: 0; + width: 100%; } .social { - padding-right:21px; + padding-right: 21px; } - header .social a { - background-position:0 0; - background-repeat:no-repeat; - float:right; - height:38px; - margin-left:13px; - margin-right:0; - margin-top:13px; - overflow:hidden; - position:relative; - -webkit-transition: all 0.1s ease-in; - -moz-transition: all 0.1s ease-in; - -o-transition: all 0.1s ease-in; - width:38px; + background-position: 0 0; + background-repeat: no-repeat; + float: right; + height: 38px; + margin-left: 13px; + margin-right: 0; + margin-top: 13px; + overflow: hidden; + position: relative; + -webkit-transition: all 0.1s ease-in; + -moz-transition: all 0.1s ease-in; + -o-transition: all 0.1s ease-in; + width: 38px; } header a:focus, header a:hover { - background-position:0 100%; - outline:none; - -webkit-transition: all 0.1s ease-in; - -moz-transition: all 0.1s ease-in; - -o-transition: all 0.1s ease-in; + background-position: 0 100%; + outline: none; + -webkit-transition: all 0.1s ease-in; + -moz-transition: all 0.1s ease-in; + -o-transition: all 0.1s ease-in; } .share { - background-image:url('../img/share.png'); + background-image: url("../img/share.png"); } .sharemask { - background-image:url('../img/sharemask.png'); - background-position:top left; - background-repeat:no-repeat; - height:38px; - left:0; - position:absolute; - top:0; - width:38px; - z-index:10; - + background-image: url("../img/sharemask.png"); + background-position: top left; + background-repeat: no-repeat; + height: 38px; + left: 0; + position: absolute; + top: 0; + width: 38px; + z-index: 10; } .facebook { - background-image:url('../img/facebook.png'); + background-image: url("../img/facebook.png"); } .facebookmask { - background-image:url('../img/facebookmask.png'); - background-position:top left; - background-repeat:no-repeat; - height:38px; - left:0; - position:absolute; - top:0; - width:38px; - z-index:10; + background-image: url("../img/facebookmask.png"); + background-position: top left; + background-repeat: no-repeat; + height: 38px; + left: 0; + position: absolute; + top: 0; + width: 38px; + z-index: 10; } .twitter { - background-image:url('../img/twitter.png'); + background-image: url("../img/twitter.png"); } .twittermask { - background-image:url('../img/twittermask.png'); - background-position:top left; - background-repeat:no-repeat; - height:38px; - left:0; - position:absolute; - top:0; - width:38px; - z-index:10; + background-image: url("../img/twittermask.png"); + background-position: top left; + background-repeat: no-repeat; + height: 38px; + left: 0; + position: absolute; + top: 0; + width: 38px; + z-index: 10; } - - /* Footer */ - footer { - background-image:url('../img/fondfooter.png'); - background-position:top left; - background-repeat:repeat-x; - bottom:0; - color:#adadad; - padding-top:20px; - position:fixed; - text-align:center; - width:100%; - z-index:11; + background-image: url("../img/fondfooter.png"); + background-position: top left; + background-repeat: repeat-x; + bottom: 0; + color: #adadad; + padding-top: 20px; + position: fixed; + text-align: center; + width: 100%; + z-index: 11; } .footer_wrapper { - height:28px; + height: 28px; } -footer a{ - color:#adadad; - text-decoration:none; - -webkit-transition: all 0.1s ease-in; - -moz-transition: all 0.1s ease-in; - -o-transition: all 0.1s ease-in; +footer a { + color: #adadad; + text-decoration: none; + -webkit-transition: all 0.1s ease-in; + -moz-transition: all 0.1s ease-in; + -o-transition: all 0.1s ease-in; } footer a:focus, footer a:hover { - color:#f2084a; - outline:none; - -webkit-transition: all 0.1s ease-in; - -moz-transition: all 0.1s ease-in; - -o-transition: all 0.1s ease-in; + color: #f2084a; + outline: none; + -webkit-transition: all 0.1s ease-in; + -moz-transition: all 0.1s ease-in; + -o-transition: all 0.1s ease-in; } - - - - - /* Home content */ .logo { - padding-bottom:55px; + padding-bottom: 55px; } .labelurl { - color:#3f3f3f; - font-size:19px; - position:relative; + color: #3f3f3f; + font-size: 19px; + position: relative; } .champs { - margin-bottom:70px; - margin-top:8px; - position:relative; + margin-bottom: 70px; + margin-top: 8px; + position: relative; } .downloadBtn { - background-color:#3A3A3A; - border: 3px solid #a5a5a5; - border-radius:10px; - color:#dedede; - cursor:pointer; - display:inline-block; - font-weight:800; - padding: 12px 14px; - position:relative; - text-decoration:none; - -webkit-transition: all 0.1s ease-in; - -moz-transition: all 0.1s ease-in; - -o-transition: all 0.1s ease-in; + background-color: #3a3a3a; + border: 3px solid #a5a5a5; + border-radius: 10px; + color: #dedede; + cursor: pointer; + display: inline-block; + font-weight: 800; + padding: 12px 14px; + position: relative; + text-decoration: none; + -webkit-transition: all 0.1s ease-in; + -moz-transition: all 0.1s ease-in; + -o-transition: all 0.1s ease-in; } .downloadBtn:focus, .downloadBtn:hover { - background-color:#f2084a; - outline:none; - -webkit-transition: all 0.1s ease-in; - -moz-transition: all 0.1s ease-in; - -o-transition: all 0.1s ease-in; + background-color: #f2084a; + outline: none; + -webkit-transition: all 0.1s ease-in; + -moz-transition: all 0.1s ease-in; + -o-transition: all 0.1s ease-in; } .downloadBtn::-moz-focus-inner { - border:none; + border: none; } -.URLinput{ - background-color:#fff; - border: 3px solid #a5a5a5; - border-radius:10px; - color:#3F3F3F; - font-weight:800; - margin-right:8px; - min-width:426px; - padding: 12px 12px 12px 12px; - position:relative; +.URLinput { + background-color: #fff; + border: 3px solid #a5a5a5; + border-radius: 10px; + color: #3f3f3f; + font-weight: 800; + margin-right: 8px; + min-width: 426px; + padding: 12px 12px 12px 12px; + position: relative; } - .URLinput:focus { - border-color:#3A3A3A; - outline: none; + border-color: #3a3a3a; + outline: none; } -.URLinput:-webkit-input-placeholder{ - color:#c1cfcf; +.URLinput:-webkit-input-placeholder { + color: #c1cfcf; } -.URLinput:-moz-placeholder { - color:#c1cfcf; +.URLinput:-moz-placeholder { + color: #c1cfcf; } .combatiblelink { - background-image:url('../img/compatiblerouage.png'); - background-position:0 100%; - background-repeat:no-repeat; - color:#a5a5a5; - padding-bottom:10px; - padding-left:41px; - padding-top:10px; - position:relative; - text-decoration:none; - -webkit-transition: all 0.1s ease-in; - -moz-transition: all 0.1s ease-in; - -o-transition: all 0.1s ease-in; - z-index:10; + background-image: url("../img/compatiblerouage.png"); + background-position: 0 100%; + background-repeat: no-repeat; + color: #a5a5a5; + padding-bottom: 10px; + padding-left: 41px; + padding-top: 10px; + position: relative; + text-decoration: none; + -webkit-transition: all 0.1s ease-in; + -moz-transition: all 0.1s ease-in; + -o-transition: all 0.1s ease-in; + z-index: 10; } .combatiblelink:focus, .combatiblelink:hover { - background-position:0 0; - color:#f2084a; - outline:none; - -webkit-transition: all 0.1s ease-in; - -moz-transition: all 0.1s ease-in; - -o-transition: all 0.1s ease-in; + background-position: 0 0; + color: #f2084a; + outline: none; + -webkit-transition: all 0.1s ease-in; + -moz-transition: all 0.1s ease-in; + -o-transition: all 0.1s ease-in; } .bookmarklet { - border: 2px dotted; - color:gray; - padding:10px 30px; - position:relative; - text-decoration:none; - z-index:10; + border: 2px dotted; + color: gray; + padding: 10px 30px; + position: relative; + text-decoration: none; + z-index: 10; } .mp3 { - background-color:#cecece; - border-radius:6px; - color:#3f3f3f; - height:26px; - margin-top:12px; - position:relative; - text-align:left; - width:622px; + background-color: #cecece; + border-radius: 6px; + color: #3f3f3f; + height: 26px; + margin-top: 12px; + position: relative; + text-align: left; + width: 622px; } .mp3-inner { - padding:3px; + padding: 3px; } .audio:not(:checked), .audio:checked { - left: -9999px; - position: absolute; + left: -9999px; + position: absolute; } .audio:not(:checked) + label, .audio:checked + label { - cursor: pointer; - line-height:20px; - padding-left: 82px; - position: relative; + cursor: pointer; + line-height: 20px; + padding-left: 82px; + position: relative; } .audio:not(:checked) + label:before, .audio:checked + label:before, .audio:not(:checked) + label:after, .audio:checked + label:after { - content: ''; - position: absolute; + content: ""; + position: absolute; } .audio:not(:checked) + label:before, .audio:checked + label:before { - background: #ffffff; - border-radius: 6px; - height: 20px; - left:0; - top: -1px; - -webkit-transition: background-color .2s; - -moz-transition: background-color .2s; - -ms-transition: background-color .2s; - -o-transition: background-color .2s; - transition: background-color .2s; - width: 45px; + background: #ffffff; + border-radius: 6px; + height: 20px; + left: 0; + top: -1px; + -webkit-transition: background-color 0.2s; + -moz-transition: background-color 0.2s; + -ms-transition: background-color 0.2s; + -o-transition: background-color 0.2s; + transition: background-color 0.2s; + width: 45px; } .audio:not(:checked) + label:after, .audio:checked + label:after { - background: #3a3a3a; - border-radius: 6px; - height: 16px; - left: 2px; - top: 1px; - -webkit-transition: all .2s; - -moz-transition: all .2s; - -ms-transition: all .2s; - -o-transition: all .2s; - transition: all .2s; - width: 16px; + background: #3a3a3a; + border-radius: 6px; + height: 16px; + left: 2px; + top: 1px; + -webkit-transition: all 0.2s; + -moz-transition: all 0.2s; + -ms-transition: all 0.2s; + -o-transition: all 0.2s; + transition: all 0.2s; + width: 16px; } .audio:focus + label { - color:black; + color: black; } /* on checked */ .audio:checked + label:before { - background:#f2084a; + background: #f2084a; } .audio:checked + label:after { - background: #fff; - left: 27px; - top: 1px; + background: #fff; + left: 27px; + top: 1px; } .audio:checked + label .ui, .audio:not(:checked) + label .ui:before, .audio:checked + label .ui:after { - border-radius: 15px; - font-size: 11px; - font-weight: bold; - height:20px; - left: 3px; - line-height: 17px; - position: absolute; - -webkit-transition: all .2s; - -moz-transition: all .2s; - -ms-transition: all .2s; - -o-transition: all .2s; - transition: all .2s; - width: 45px; + border-radius: 15px; + font-size: 11px; + font-weight: bold; + height: 20px; + left: 3px; + line-height: 17px; + position: absolute; + -webkit-transition: all 0.2s; + -moz-transition: all 0.2s; + -ms-transition: all 0.2s; + -o-transition: all 0.2s; + transition: all 0.2s; + width: 45px; } .audio:not(:checked) + label .ui:before { - background-image:url('../img/mp3hover.png'); - background-position:right top; - background-repeat:no-repeat; - content: "no"; - left: 0; - min-width:56px; - padding-left:23px; - padding-top:2px; - -webkit-transition: all .2s; - -moz-transition: all .2s; - -ms-transition: all .2s; - -o-transition: all .2s; - transition: all .2s; + background-image: url("../img/mp3hover.png"); + background-position: right top; + background-repeat: no-repeat; + content: "no"; + left: 0; + min-width: 56px; + padding-left: 23px; + padding-top: 2px; + -webkit-transition: all 0.2s; + -moz-transition: all 0.2s; + -ms-transition: all 0.2s; + -o-transition: all 0.2s; + transition: all 0.2s; } .audio:checked + label .ui:after { - background-image:url('../img/mp3.png'); - background-position:right top; - background-repeat:no-repeat; - color: #fff; - content: "yes"; - -webkit-transition: all .2s; - -moz-transition: all .2s; - -ms-transition: all .2s; - -o-transition: all .2s; - transition: all .2s; - width:73px; + background-image: url("../img/mp3.png"); + background-position: right top; + background-repeat: no-repeat; + color: #fff; + content: "yes"; + -webkit-transition: all 0.2s; + -moz-transition: all 0.2s; + -ms-transition: all 0.2s; + -o-transition: all 0.2s; + transition: all 0.2s; + width: 73px; } .seekOptions { - display: none; - margin-top: 15px; - text-align: center; + display: none; + margin-top: 15px; + text-align: center; } .audio:checked ~ .seekOptions { - display: block; + display: block; } - /* Playlists */ .playlist-entry .thumb { - float: left; - margin-right: 1em; + float: left; + margin-right: 1em; } .playlist-entry { - clear: both; - padding-top: 2em; - text-align: left; - width: 600px; + clear: both; + padding-top: 2em; + text-align: left; + width: 600px; } .playlist-entry-title { - margin-top: 0; + margin-top: 0; } .playlist-entry-title a { - text-decoration: none; + text-decoration: none; } .playlist-entry-title a:hover { - text-decoration: underline; + text-decoration: underline; } .playlist-entry .downloadBtn { - border-width: 2px; - font-size: 16px; + border-width: 2px; + font-size: 16px; } - - - /* Supported websites list */ .logobis { - height:107px; - margin:0 auto 10px auto; - position:relative; - width:447px; + height: 107px; + margin: 0 auto 10px auto; + position: relative; + width: 447px; } - .logocompatible { - background-image:url('../img/logocompatible.png'); - background-position:0 0; - background-repeat:repeat-y; - display:block; - height:107px; - -webkit-transition: all 0.1s ease-in; - -moz-transition: all 0.1s ease-in; - -o-transition: all 0.1s ease-in; - width:447px; + background-image: url("../img/logocompatible.png"); + background-position: 0 0; + background-repeat: repeat-y; + display: block; + height: 107px; + -webkit-transition: all 0.1s ease-in; + -moz-transition: all 0.1s ease-in; + -o-transition: all 0.1s ease-in; + width: 447px; } .logocompatible:focus, .logocompatible:hover { - background-position:0 100%; - outline:none; - -webkit-transition: all 0.1s ease-in; - -moz-transition: all 0.1s ease-in; - -o-transition: all 0.1s ease-in; + background-position: 0 100%; + outline: none; + -webkit-transition: all 0.1s ease-in; + -moz-transition: all 0.1s ease-in; + -o-transition: all 0.1s ease-in; } - .logocompatiblemask { - background-image:url('../img/logocompatiblemask.png'); - background-position:0 100%; - background-repeat:no-repeat; - height:107px; - left:0; - position:absolute; - top:0; - width:447px; - z-index:10; + background-image: url("../img/logocompatiblemask.png"); + background-position: 0 100%; + background-repeat: no-repeat; + height: 107px; + left: 0; + position: absolute; + top: 0; + width: 447px; + z-index: 10; } .titre { - color:#383838; - font-family: 'Open Sans', sans-serif; - font-size:48px; - font-weight:300; + color: #383838; + font-family: "Open Sans", sans-serif; + font-size: 48px; + font-weight: 300; } .tripleliste { - margin-left:auto; - margin-right:auto; - margin-top:80px; - position:relative; - width:800px; + margin-left: auto; + margin-right: auto; + margin-top: 80px; + position: relative; + width: 800px; } - .tripleliste ul { - margin-bottom:1em; - margin-left:120px; - width:600px; + margin-bottom: 1em; + margin-left: 120px; + width: 600px; } .tripleliste ul li { - color:#383838; - float:left; - list-style-type:none; - position:relative; - text-align:left; - width:200px; + color: #383838; + float: left; + list-style-type: none; + position: relative; + text-align: left; + width: 200px; } html, body { - height:100%; - margin:0; + height: 100%; + margin: 0; } .wrapper { - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; - display:table; - height:100%; - margin:auto; - padding-bottom:110px; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + display: table; + height: 100%; + margin: auto; + padding-bottom: 110px; } .main { - display:table-cell; - vertical-align:middle; + display: table-cell; + vertical-align: middle; } .extractors { - padding-top:60px; + padding-top: 60px; } .extractors .wrapper { - padding-bottom:5em; + padding-bottom: 5em; } .logocompatible, .social a { - font-size:0; - text-decoration:none; + font-size: 0; + text-decoration: none; } .social a { - color:#D1D1D1; + color: #d1d1d1; } .logocompatible { - color: #4F4F4F + color: #4f4f4f; } h1 { - margin:0; + margin: 0; } .error { - max-width: 100ex; + max-width: 100ex; } .error p { - text-align:justify; + text-align: justify; } .smaller { - font-size:smaller; + font-size: smaller; } .thumb { - max-width:700px; + max-width: 700px; } .format { - text-align:left; + text-align: left; } .best { - margin-bottom: 1em; + margin-bottom: 1em; } .monospace { - font-family:monospace; + font-family: monospace; } .customBitrate { - width: 6ex; + width: 6ex; } .locales { - float: left; - padding-left: 1em; - padding-top: 1em; - text-align: left; + float: left; + padding-left: 1em; + padding-top: 1em; + text-align: left; } .locales a, .locales a:visited { - color: #696969; - text-decoration: none; + color: #696969; + text-decoration: none; } .supportedLocales { - background-color: #fff; - list-style-type: none; - margin: 0; - opacity: 0; - padding-left: 0; - transition: visibility 0.5s; - visibility: hidden; + background-color: #fff; + list-style-type: none; + margin: 0; + opacity: 0; + padding-left: 0; + transition: visibility 0.5s; + visibility: hidden; } .supportedLocales li { - border-bottom: thin solid #E1E1E1; + border-bottom: thin solid #e1e1e1; } .supportedLocales li:last-child { - border-bottom: none; + border-bottom: none; } .supportedLocales li a { - display: block; - padding: 1em; - padding-right: 2em; + display: block; + padding: 1em; + padding-right: 2em; } .supportedLocales li:hover { - background-color: #cecece; + background-color: #cecece; } .localesBtn { - background-color: transparent; - border: none; - cursor: pointer; - display: inline-block; - padding: 1em; + background-color: transparent; + border: none; + cursor: pointer; + display: inline-block; + padding: 1em; } .localesBtn:focus { - background-color: #fff; - pointer-events: none; + background-color: #fff; + pointer-events: none; } .localesBtn:focus + .supportedLocales { - opacity: 1; - visibility: visible; + opacity: 1; + visibility: visible; } @media (max-width: 640px) { - .formats, - .thumb { - width:90%; - } + .formats, + .thumb { + width: 90%; + } - .URLinput{ - min-width:0; - } + .URLinput { + min-width: 0; + } - .logo { - max-width:330px; - } + .logo { + max-width: 330px; + } - .logocompatible, - .logocompatible img { - max-width:447px; - } + .logocompatible, + .logocompatible img { + max-width: 447px; + } - .logocompatible, - .logo, - .champs, - .URLinput, - .mp3 { - height:auto; - margin:auto; - width:90%; - } + .logocompatible, + .logo, + .champs, + .URLinput, + .mp3 { + height: auto; + margin: auto; + width: 90%; + } - .logo { - margin-top:50px; - } + .logo { + margin-top: 50px; + } - .logocompatible img { - height: auto; - width:100%; - } + .logocompatible img { + height: auto; + width: 100%; + } - .downloadBtn { - margin-top: 0.3em; - } - .mp3 { - margin-bottom: 1em; - } + .downloadBtn { + margin-top: 0.3em; + } + .mp3 { + margin-bottom: 1em; + } - footer { - display:none; - } + footer { + display: none; + } - .tripleliste ul, - .tripleliste { - margin-left:auto; - margin-top:auto; - width:auto; - } + .tripleliste ul, + .tripleliste { + margin-left: auto; + margin-top: auto; + width: auto; + } - .logocompatiblemask { - background:none; - } + .logocompatiblemask { + background: none; + } - .logocompatible { - background-color:#4F4F4F; - background-image:none; - height:auto; - } + .logocompatible { + background-color: #4f4f4f; + background-image: none; + height: auto; + } - .logocompatiblemask, - .logobis { - width:auto; - } + .logocompatiblemask, + .logobis { + width: auto; + } - .logocompatiblemask { - position:static; - } + .logocompatiblemask { + position: static; + } - .logobis { - height:auto; - } + .logobis { + height: auto; + } - .titre { - margin:auto; - } + .titre { + margin: auto; + } - .error p { - padding:0.5em; - text-align:left; - } + .error p { + padding: 0.5em; + text-align: left; + } - .playlist-entry { - text-align: center; - width: auto; - } - - .playlist-entry .thumb { - float: none; - margin-right: 0; - } + .playlist-entry { + text-align: center; + width: auto; + } + .playlist-entry .thumb { + float: none; + margin-right: 0; + } } @media all and (display-mode: standalone) { - .bookmarklet_wrapper { - display: none; - } + .bookmarklet_wrapper { + display: none; + } +} + +/* Visually hidden, displays content only to screen-readers */ +.sr-only { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + white-space: nowrap; + width: 1px; } diff --git a/index.php b/index.php index a3d4a27..791649b 100644 --- a/index.php +++ b/index.php @@ -2,10 +2,11 @@ require_once __DIR__.'/vendor/autoload.php'; use Alltube\Config; +use Alltube\Controller\DownloadController; use Alltube\Controller\FrontController; +use Alltube\Controller\JsonController; use Alltube\LocaleManager; use Alltube\LocaleMiddleware; -use Alltube\PlaylistArchiveStream; use Alltube\UglyRouter; use Alltube\ViewFactory; use Slim\App; @@ -15,7 +16,9 @@ if (isset($_SERVER['REQUEST_URI']) && strpos($_SERVER['REQUEST_URI'], '/index.ph die; } -stream_wrapper_register('playlist', PlaylistArchiveStream::class); +if (is_file(__DIR__.'/config/config.yml')) { + Config::setFile(__DIR__.'/config/config.yml'); +} $app = new App(); $container = $app->getContainer(); @@ -28,42 +31,54 @@ $container['view'] = ViewFactory::create($container); if (!class_exists('Locale')) { die('You need to install the intl extension for PHP.'); } -$container['locale'] = new LocaleManager($_COOKIE); +$container['locale'] = new LocaleManager(); $app->add(new LocaleMiddleware($container)); -$controller = new FrontController($container, null, $_COOKIE); +$frontController = new FrontController($container); +$jsonController = new JsonController($container); +$downloadController = new DownloadController($container); -$container['errorHandler'] = [$controller, 'error']; +$container['errorHandler'] = [$frontController, 'error']; $app->get( '/', - [$controller, 'index'] + [$frontController, 'index'] )->setName('index'); + $app->get( '/extractors', - [$controller, 'extractors'] + [$frontController, 'extractors'] )->setName('extractors'); + $app->any( - '/video', - [$controller, 'video'] -)->setName('video'); + '/info', + [$frontController, 'info'] +)->setName('info'); +// Legacy route. +$app->any('/video', [$frontController, 'info']); + $app->any( '/watch', - [$controller, 'video'] + [$frontController, 'video'] ); -$app->get( - '/redirect', - [$controller, 'redirect'] -)->setName('redirect'); -$app->get( - '/json', - [$controller, 'json'] -)->setName('json'); + +$app->any( + '/download', + [$downloadController, 'download'] +)->setName('download'); +// Legacy route. +$app->get('/redirect', [$downloadController, 'download']); + $app->get( '/locale/{locale}', - [$controller, 'locale'] + [$frontController, 'locale'] )->setName('locale'); +$app->get( + '/json', + [$jsonController, 'json'] +)->setName('json'); + try { $app->run(); } catch (SmartyException $e) { diff --git a/package.json b/package.json index 804e447..0e1760e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "alltube", "description": "HTML GUI for youtube-dl", - "version": "1.2.5", + "version": "2.0.0", "author": "Pierre Rudloff", "bugs": "https://github.com/Rudloff/alltube/issues", "dependencies": { @@ -21,6 +21,7 @@ "grunt-markdownlint": "~2.1.0", "grunt-phpcs": "~0.4.0", "grunt-phpdocumentor": "~0.4.1", + "grunt-phpstan": "~0.2.0", "grunt-phpunit": "~0.3.6" }, "homepage": "https://www.alltubedownload.net/", diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..0f1af62 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,4 @@ +parameters: + ignoreErrors: + # The Archive constructor messes up the output buffering. + - '#Alltube\\PlaylistArchiveStream::__construct\(\) does not call parent constructor from Barracuda\\ArchiveStream\\ZipArchive\.#' diff --git a/templates/inc/head.tpl b/templates/inc/head.tpl index b4a5cf1..c3ee074 100644 --- a/templates/inc/head.tpl +++ b/templates/inc/head.tpl @@ -1,25 +1,25 @@ {locale path="../i18n" domain="Alltube"} - + getBcp47()}"{/if}>
- - -{if isset($description)} - - - -{/if} - -