Merge branch 'release/2.0.0'
This commit is contained in:
commit
033e363c93
52 changed files with 4715 additions and 2545 deletions
|
@ -2,16 +2,17 @@
|
||||||
install:
|
install:
|
||||||
- sc config wuauserv start= auto
|
- sc config wuauserv start= auto
|
||||||
- net start wuauserv
|
- net start wuauserv
|
||||||
- cinst php composer ffmpeg phantomjs
|
- cinst php --version 7.1.28
|
||||||
|
- cinst composer ffmpeg phantomjs
|
||||||
- refreshenv
|
- refreshenv
|
||||||
- copy C:\tools\php72\php.ini-development C:\tools\php72\php.ini
|
- copy C:\tools\php71\php.ini-development C:\tools\php71\php.ini
|
||||||
- echo extension=C:\tools\php72\ext\php_gmp.dll >> C:\tools\php72\php.ini
|
- echo extension=C:\tools\php71\ext\php_gmp.dll >> C:\tools\php71\php.ini
|
||||||
- echo extension=C:\tools\php72\ext\php_gettext.dll >> C:\tools\php72\php.ini
|
- echo extension=C:\tools\php71\ext\php_gettext.dll >> C:\tools\php71\php.ini
|
||||||
- echo extension=C:\tools\php72\ext\php_intl.dll >> C:\tools\php72\php.ini
|
- echo extension=C:\tools\php71\ext\php_intl.dll >> C:\tools\php71\php.ini
|
||||||
- echo extension=C:\tools\php72\ext\php_openssl.dll >> C:\tools\php72\php.ini
|
- echo extension=C:\tools\php71\ext\php_openssl.dll >> C:\tools\php71\php.ini
|
||||||
- echo extension=C:\tools\php72\ext\php_mbstring.dll >> C:\tools\php72\php.ini
|
- echo extension=C:\tools\php71\ext\php_mbstring.dll >> C:\tools\php71\php.ini
|
||||||
- composer install --no-dev
|
- 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
|
- C:\Python36\python.exe -m pip install youtube-dl
|
||||||
|
|
||||||
test_script:
|
test_script:
|
||||||
|
|
18
Gruntfile.js
18
Gruntfile.js
|
@ -33,6 +33,19 @@ module.exports = function (grunt) {
|
||||||
src: ['tests/*.php']
|
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: {
|
jslint: {
|
||||||
js: {
|
js: {
|
||||||
src: ['js/*.js']
|
src: ['js/*.js']
|
||||||
|
@ -57,7 +70,7 @@ module.exports = function (grunt) {
|
||||||
options: {
|
options: {
|
||||||
archive: 'alltube-<%= githash.main.tag %>.zip'
|
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: {
|
phpdocumentor: {
|
||||||
|
@ -125,9 +138,10 @@ module.exports = function (grunt) {
|
||||||
grunt.loadNpmTasks('grunt-potomo');
|
grunt.loadNpmTasks('grunt-potomo');
|
||||||
grunt.loadNpmTasks('grunt-contrib-csslint');
|
grunt.loadNpmTasks('grunt-contrib-csslint');
|
||||||
grunt.loadNpmTasks('grunt-markdownlint');
|
grunt.loadNpmTasks('grunt-markdownlint');
|
||||||
|
grunt.loadNpmTasks('grunt-phpstan');
|
||||||
|
|
||||||
grunt.registerTask('default', ['cssmin', 'potomo']);
|
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('test', ['phpunit']);
|
||||||
grunt.registerTask('doc', ['phpdocumentor']);
|
grunt.registerTask('doc', ['phpdocumentor']);
|
||||||
grunt.registerTask('release', ['default', 'githash', 'compress']);
|
grunt.registerTask('release', ['default', 'githash', 'compress']);
|
||||||
|
|
10
README.md
10
README.md
|
@ -162,19 +162,17 @@ You can then use it in your PHP code:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use Alltube\Config;
|
use Alltube\Config;
|
||||||
use Alltube\VideoDownload;
|
use Alltube\Video;
|
||||||
|
|
||||||
require_once __DIR__.'/vendor/autoload.php';
|
require_once __DIR__.'/vendor/autoload.php';
|
||||||
|
|
||||||
$downloader = new VideoDownload(
|
Config::setOptions(
|
||||||
new Config(
|
|
||||||
[
|
[
|
||||||
'youtubedl' => '/usr/local/bin/youtube-dl',
|
'youtubedl' => '/usr/local/bin/youtube-dl',
|
||||||
]
|
]
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
$video = new Video('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
|
||||||
$downloader->getURL('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).
|
The library documentation is available on [alltube.surge.sh](https://alltube.surge.sh/classes/Alltube.VideoDownload.html).
|
||||||
|
|
|
@ -16,7 +16,7 @@ class Config
|
||||||
/**
|
/**
|
||||||
* Singleton instance.
|
* Singleton instance.
|
||||||
*
|
*
|
||||||
* @var Config
|
* @var Config|null
|
||||||
*/
|
*/
|
||||||
private static $instance;
|
private static $instance;
|
||||||
|
|
||||||
|
@ -129,19 +129,51 @@ class Config
|
||||||
/**
|
/**
|
||||||
* Config constructor.
|
* Config constructor.
|
||||||
*
|
*
|
||||||
* @param array $options Options (see `config/config.example.yml` for available options)
|
* @param array $options Options
|
||||||
*/
|
*/
|
||||||
public function __construct(array $options)
|
private function __construct(array $options = [])
|
||||||
|
{
|
||||||
|
$this->applyOptions($options);
|
||||||
|
$this->getEnv();
|
||||||
|
$this->validateOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throw an exception if some of the options are invalid.
|
||||||
|
*
|
||||||
|
* @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)
|
||||||
{
|
{
|
||||||
if (isset($options) && is_array($options)) {
|
|
||||||
foreach ($options as $option => $value) {
|
foreach ($options as $option => $value) {
|
||||||
if (isset($this->$option) && isset($value)) {
|
if (isset($this->$option) && isset($value)) {
|
||||||
$this->$option = $value;
|
$this->$option = $value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$this->getEnv();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Override options from environement variables.
|
* Override options from environement variables.
|
||||||
|
@ -161,34 +193,51 @@ class Config
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Config singleton instance from YAML config file.
|
* Get Config singleton instance.
|
||||||
*
|
|
||||||
* @param string $yamlfile YAML config file name
|
|
||||||
*
|
*
|
||||||
* @return Config
|
* @return Config
|
||||||
*/
|
*/
|
||||||
public static function getInstance($yamlfile = 'config/config.yml')
|
public static function getInstance()
|
||||||
{
|
{
|
||||||
$yamlPath = __DIR__.'/../'.$yamlfile;
|
if (!isset(self::$instance)) {
|
||||||
if (is_null(self::$instance) || self::$instance->file != $yamlfile) {
|
self::$instance = new self();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return self::$instance;
|
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.
|
* Destroy singleton instance.
|
||||||
*
|
*
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
namespace Alltube;
|
namespace Alltube;
|
||||||
|
|
||||||
use Aura\Session\Segment;
|
use Aura\Session\Segment;
|
||||||
use Aura\Session\SessionFactory;
|
|
||||||
use Symfony\Component\Process\Process;
|
use Symfony\Component\Process\Process;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -24,7 +23,7 @@ class LocaleManager
|
||||||
/**
|
/**
|
||||||
* Current locale.
|
* Current locale.
|
||||||
*
|
*
|
||||||
* @var Locale
|
* @var Locale|null
|
||||||
*/
|
*/
|
||||||
private $curLocale;
|
private $curLocale;
|
||||||
|
|
||||||
|
@ -37,13 +36,10 @@ class LocaleManager
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LocaleManager constructor.
|
* LocaleManager constructor.
|
||||||
*
|
|
||||||
* @param array $cookies Cookie array
|
|
||||||
*/
|
*/
|
||||||
public function __construct(array $cookies = [])
|
public function __construct()
|
||||||
{
|
{
|
||||||
$session_factory = new SessionFactory();
|
$session = SessionManager::getSession();
|
||||||
$session = $session_factory->newInstance($cookies);
|
|
||||||
$this->sessionSegment = $session->getSegment(self::class);
|
$this->sessionSegment = $session->getSegment(self::class);
|
||||||
$cookieLocale = $this->sessionSegment->get('locale');
|
$cookieLocale = $this->sessionSegment->get('locale');
|
||||||
if (isset($cookieLocale)) {
|
if (isset($cookieLocale)) {
|
||||||
|
@ -78,7 +74,7 @@ class LocaleManager
|
||||||
/**
|
/**
|
||||||
* Get the current locale.
|
* Get the current locale.
|
||||||
*
|
*
|
||||||
* @return Locale
|
* @return Locale|null
|
||||||
*/
|
*/
|
||||||
public function getLocale()
|
public function getLocale()
|
||||||
{
|
{
|
||||||
|
|
|
@ -37,7 +37,7 @@ class LocaleMiddleware
|
||||||
*
|
*
|
||||||
* @param array $proposedLocale Locale array created by AcceptLanguage::parse()
|
* @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)
|
public function testLocale(array $proposedLocale)
|
||||||
{
|
{
|
||||||
|
@ -65,7 +65,7 @@ class LocaleMiddleware
|
||||||
{
|
{
|
||||||
$headers = $request->getHeader('Accept-Language');
|
$headers = $request->getHeader('Accept-Language');
|
||||||
$curLocale = $this->localeManager->getLocale();
|
$curLocale = $this->localeManager->getLocale();
|
||||||
if (!isset($curLocale)) {
|
if (is_null($curLocale)) {
|
||||||
if (isset($headers[0])) {
|
if (isset($headers[0])) {
|
||||||
$this->localeManager->setLocale(
|
$this->localeManager->setLocale(
|
||||||
AcceptLanguage::detect([$this, 'testLocale'], new Locale('en_US'), $headers[0])
|
AcceptLanguage::detect([$this, 'testLocale'], new Locale('en_US'), $headers[0])
|
||||||
|
|
|
@ -1,209 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* PlaylistArchiveStream class.
|
|
||||||
*
|
|
||||||
* @codingStandardsIgnoreFile
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Alltube;
|
|
||||||
|
|
||||||
use Barracuda\ArchiveStream\TarArchive;
|
|
||||||
use GuzzleHttp\Client;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class used to create a Tar archive from playlists and stream it to the browser.
|
|
||||||
*
|
|
||||||
* @link http://php.net/manual/en/class.streamwrapper.php
|
|
||||||
*/
|
|
||||||
class PlaylistArchiveStream extends TarArchive
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Files to add in the archive.
|
|
||||||
*
|
|
||||||
* @var array[]
|
|
||||||
*/
|
|
||||||
private $files;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stream used to store data before it is sent to the browser.
|
|
||||||
*
|
|
||||||
* @var resource
|
|
||||||
*/
|
|
||||||
private $buffer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Guzzle client.
|
|
||||||
*
|
|
||||||
* @var Client
|
|
||||||
*/
|
|
||||||
private $client;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* VideoDownload instance.
|
|
||||||
*
|
|
||||||
* @var VideoDownload
|
|
||||||
*/
|
|
||||||
private $download;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current file position in $files array.
|
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
private $curFile = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Video format to download.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
private $format;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PlaylistArchiveStream constructor.
|
|
||||||
*
|
|
||||||
* @param Config $config Config instance.
|
|
||||||
*/
|
|
||||||
public function __construct(Config $config = null)
|
|
||||||
{
|
|
||||||
$this->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);
|
|
||||||
}
|
|
||||||
}
|
|
37
classes/SessionManager.php
Normal file
37
classes/SessionManager.php
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* SessionManager class.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Alltube;
|
||||||
|
|
||||||
|
use Aura\Session\Session;
|
||||||
|
use Aura\Session\SessionFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manage sessions.
|
||||||
|
*/
|
||||||
|
class SessionManager
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Current session.
|
||||||
|
*
|
||||||
|
* @var Session
|
||||||
|
*/
|
||||||
|
private static $session;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current session.
|
||||||
|
*
|
||||||
|
* @return Session
|
||||||
|
*/
|
||||||
|
public static function getSession()
|
||||||
|
{
|
||||||
|
if (!isset(self::$session)) {
|
||||||
|
$session_factory = new SessionFactory();
|
||||||
|
self::$session = $session_factory->newInstance($_COOKIE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$session;
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,14 +5,30 @@
|
||||||
|
|
||||||
namespace Alltube;
|
namespace Alltube;
|
||||||
|
|
||||||
|
use Alltube\Exception\EmptyUrlException;
|
||||||
|
use Alltube\Exception\PasswordException;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use GuzzleHttp\Psr7\Response;
|
||||||
use stdClass;
|
use stdClass;
|
||||||
use Symfony\Component\Process\Process;
|
use Symfony\Component\Process\Process;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract info about videos.
|
* 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.
|
* Config instance.
|
||||||
|
@ -21,31 +37,55 @@ class VideoDownload
|
||||||
*/
|
*/
|
||||||
private $config;
|
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.
|
* VideoDownload constructor.
|
||||||
*
|
*
|
||||||
* @param Config $config Config instance.
|
* @param string $webpageUrl URL of the page containing the video
|
||||||
*
|
* @param string $requestedFormat Requested video format
|
||||||
* @throws Exception If youtube-dl is missing
|
* @param string $password Password
|
||||||
* @throws Exception If Python is missing
|
|
||||||
*/
|
*/
|
||||||
public function __construct(Config $config = null)
|
public function __construct($webpageUrl, $requestedFormat = 'best', $password = null)
|
||||||
{
|
{
|
||||||
if (isset($config)) {
|
$this->webpageUrl = $webpageUrl;
|
||||||
$this->config = $config;
|
$this->requestedFormat = $requestedFormat;
|
||||||
} else {
|
$this->password = $password;
|
||||||
$this->config = Config::getInstance();
|
$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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a youtube-dl process with the specified arguments.
|
* Return a youtube-dl process with the specified arguments.
|
||||||
|
@ -54,12 +94,14 @@ class VideoDownload
|
||||||
*
|
*
|
||||||
* @return Process
|
* @return Process
|
||||||
*/
|
*/
|
||||||
private function getProcess(array $arguments)
|
private static function getProcess(array $arguments)
|
||||||
{
|
{
|
||||||
|
$config = Config::getInstance();
|
||||||
|
|
||||||
return new Process(
|
return new Process(
|
||||||
array_merge(
|
array_merge(
|
||||||
[$this->config->python, $this->config->youtubedl],
|
[$config->python, $config->youtubedl],
|
||||||
$this->config->params,
|
$config->params,
|
||||||
$arguments
|
$arguments
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -70,42 +112,29 @@ class VideoDownload
|
||||||
*
|
*
|
||||||
* @return string[] Extractors
|
* @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 array $arguments Arguments
|
||||||
* @param string $format Format
|
|
||||||
* @param string $prop Property
|
|
||||||
* @param string $password Video password
|
|
||||||
*
|
*
|
||||||
* @throws PasswordException If the video is protected by a password and no password was specified
|
* @throws PasswordException If the video is protected by a password and no password was specified
|
||||||
* @throws Exception If the password is wrong
|
* @throws Exception If the password is wrong
|
||||||
* @throws Exception If youtube-dl returns an error
|
* @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 = [
|
$config = Config::getInstance();
|
||||||
'--'.$prop,
|
|
||||||
$url,
|
|
||||||
];
|
|
||||||
if (isset($format)) {
|
|
||||||
$arguments[] = '-f '.$format;
|
|
||||||
}
|
|
||||||
if (isset($password)) {
|
|
||||||
$arguments[] = '--video-password';
|
|
||||||
$arguments[] = $password;
|
|
||||||
}
|
|
||||||
|
|
||||||
$process = $this->getProcess($arguments);
|
$process = self::getProcess($arguments);
|
||||||
//This is needed by the openload extractor because it runs PhantomJS
|
//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->inheritEnvironmentVariables();
|
||||||
$process->run();
|
$process->run();
|
||||||
if (!$process->isSuccessful()) {
|
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.
|
* Get all information about a video.
|
||||||
*
|
*
|
||||||
* @param string $url URL of page
|
* @return stdClass Decoded JSON
|
||||||
* @param string $format Format to use for the video
|
|
||||||
* @param string $password Video password
|
|
||||||
*
|
|
||||||
* @return object 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
|
* But it can return two URLs when multiple formats are specified
|
||||||
* (eg. bestvideo+bestaudio).
|
* (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
|
* @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])) {
|
if (empty($this->urls[0])) {
|
||||||
throw new EmptyUrlException(_('youtube-dl returned an empty URL.'));
|
throw new EmptyUrlException(_('youtube-dl returned an empty URL.'));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $urls;
|
return $this->urls;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get filename of video file from URL of page.
|
* 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
|
* @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.
|
* Get filename of video with the specified extension.
|
||||||
*
|
*
|
||||||
* @param string $extension New file 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
|
* @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(
|
return html_entity_decode(
|
||||||
pathinfo(
|
pathinfo(
|
||||||
$this->getFilename($url, $format, $password),
|
$this->getFilename(),
|
||||||
PATHINFO_FILENAME
|
PATHINFO_FILENAME
|
||||||
).'.'.$extension,
|
).'.'.$extension,
|
||||||
ENT_COMPAT,
|
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.
|
* Return arguments used to run rtmp for a specific video.
|
||||||
*
|
*
|
||||||
* @param object $video Video object returned by youtube-dl
|
|
||||||
*
|
|
||||||
* @return array Arguments
|
* @return array Arguments
|
||||||
*/
|
*/
|
||||||
private function getRtmpArguments(stdClass $video)
|
private function getRtmpArguments()
|
||||||
{
|
{
|
||||||
$arguments = [];
|
$arguments = [];
|
||||||
|
|
||||||
if ($video->protocol == 'rtmp') {
|
if ($this->protocol == 'rtmp') {
|
||||||
foreach ([
|
foreach ([
|
||||||
'url' => '-rtmp_tcurl',
|
'url' => '-rtmp_tcurl',
|
||||||
'webpage_url' => '-rtmp_pageurl',
|
'webpage_url' => '-rtmp_pageurl',
|
||||||
|
@ -231,14 +288,14 @@ class VideoDownload
|
||||||
'play_path' => '-rtmp_playpath',
|
'play_path' => '-rtmp_playpath',
|
||||||
'app' => '-rtmp_app',
|
'app' => '-rtmp_app',
|
||||||
] as $property => $option) {
|
] as $property => $option) {
|
||||||
if (isset($video->{$property})) {
|
if (isset($this->{$property})) {
|
||||||
$arguments[] = $option;
|
$arguments[] = $option;
|
||||||
$arguments[] = $video->{$property};
|
$arguments[] = $this->{$property};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($video->rtmp_conn)) {
|
if (isset($this->rtmp_conn)) {
|
||||||
foreach ($video->rtmp_conn as $conn) {
|
foreach ($this->rtmp_conn as $conn) {
|
||||||
$arguments[] = '-rtmp_conn';
|
$arguments[] = '-rtmp_conn';
|
||||||
$arguments[] = $conn;
|
$arguments[] = $conn;
|
||||||
}
|
}
|
||||||
|
@ -255,7 +312,7 @@ class VideoDownload
|
||||||
*
|
*
|
||||||
* @return bool False if the command returns an error, true otherwise
|
* @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 = new Process($command);
|
||||||
$process->run();
|
$process->run();
|
||||||
|
@ -266,7 +323,6 @@ class VideoDownload
|
||||||
/**
|
/**
|
||||||
* Get a process that runs avconv in order to convert a video.
|
* 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 int $audioBitrate Audio bitrate of the converted file
|
||||||
* @param string $filetype Filetype of the converted file
|
* @param string $filetype Filetype of the converted file
|
||||||
* @param bool $audioOnly True to return an audio-only file
|
* @param bool $audioOnly True to return an audio-only file
|
||||||
|
@ -278,7 +334,6 @@ class VideoDownload
|
||||||
* @return Process Process
|
* @return Process Process
|
||||||
*/
|
*/
|
||||||
private function getAvconvProcess(
|
private function getAvconvProcess(
|
||||||
stdClass $video,
|
|
||||||
$audioBitrate,
|
$audioBitrate,
|
||||||
$filetype = 'mp3',
|
$filetype = 'mp3',
|
||||||
$audioOnly = true,
|
$audioOnly = true,
|
||||||
|
@ -312,14 +367,16 @@ class VideoDownload
|
||||||
$afterArguments[] = $to;
|
$afterArguments[] = $to;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$urls = $this->getUrl();
|
||||||
|
|
||||||
$arguments = array_merge(
|
$arguments = array_merge(
|
||||||
[
|
[
|
||||||
$this->config->avconv,
|
$this->config->avconv,
|
||||||
'-v', $this->config->avconvVerbosity,
|
'-v', $this->config->avconvVerbosity,
|
||||||
],
|
],
|
||||||
$this->getRtmpArguments($video),
|
$this->getRtmpArguments(),
|
||||||
[
|
[
|
||||||
'-i', $video->url,
|
'-i', $urls[0],
|
||||||
'-f', $filetype,
|
'-f', $filetype,
|
||||||
'-b:a', $audioBitrate.'k',
|
'-b:a', $audioBitrate.'k',
|
||||||
],
|
],
|
||||||
|
@ -328,11 +385,10 @@ class VideoDownload
|
||||||
'pipe:1',
|
'pipe:1',
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
if ($video->url != '-') {
|
|
||||||
//Vimeo needs a correct user-agent
|
//Vimeo needs a correct user-agent
|
||||||
$arguments[] = '-user_agent';
|
$arguments[] = '-user_agent';
|
||||||
$arguments[] = $this->getProp(null, null, 'dump-user-agent');
|
$arguments[] = $this->getProp('dump-user-agent');
|
||||||
}
|
|
||||||
|
|
||||||
return new Process($arguments);
|
return new Process($arguments);
|
||||||
}
|
}
|
||||||
|
@ -340,34 +396,29 @@ class VideoDownload
|
||||||
/**
|
/**
|
||||||
* Get audio stream of converted video.
|
* 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 $from Start the conversion at this time
|
||||||
* @param string $to End 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
|
* @throws Exception If the popen stream was not created correctly
|
||||||
*
|
*
|
||||||
* @return resource popen stream
|
* @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($this->_type) && $this->_type == 'playlist') {
|
||||||
|
|
||||||
if (isset($video->_type) && $video->_type == 'playlist') {
|
|
||||||
throw new Exception(_('Conversion of playlists is not supported.'));
|
throw new Exception(_('Conversion of playlists is not supported.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($video->protocol)) {
|
if (isset($this->protocol)) {
|
||||||
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.'));
|
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.'));
|
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');
|
$stream = popen($avconvProc->getCommandLine(), 'r');
|
||||||
|
|
||||||
|
@ -381,25 +432,25 @@ class VideoDownload
|
||||||
/**
|
/**
|
||||||
* Get video stream from an M3U playlist.
|
* 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 avconv/ffmpeg is missing
|
||||||
* @throws Exception If the popen stream was not created correctly
|
* @throws Exception If the popen stream was not created correctly
|
||||||
*
|
*
|
||||||
* @return resource popen stream
|
* @return resource popen stream
|
||||||
*/
|
*/
|
||||||
public function getM3uStream(stdClass $video)
|
public function getM3uStream()
|
||||||
{
|
{
|
||||||
if (!$this->checkCommand([$this->config->avconv, '-version'])) {
|
if (!$this->checkCommand([$this->config->avconv, '-version'])) {
|
||||||
throw new Exception(_('Can\'t find avconv or ffmpeg at ').$this->config->avconv.'.');
|
throw new Exception(_('Can\'t find avconv or ffmpeg at ').$this->config->avconv.'.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$urls = $this->getUrl();
|
||||||
|
|
||||||
$process = new Process(
|
$process = new Process(
|
||||||
[
|
[
|
||||||
$this->config->avconv,
|
$this->config->avconv,
|
||||||
'-v', $this->config->avconvVerbosity,
|
'-v', $this->config->avconvVerbosity,
|
||||||
'-i', $video->url,
|
'-i', $urls[0],
|
||||||
'-f', $video->ext,
|
'-f', $this->ext,
|
||||||
'-c', 'copy',
|
'-c', 'copy',
|
||||||
'-bsf:a', 'aac_adtstoasc',
|
'-bsf:a', 'aac_adtstoasc',
|
||||||
'-movflags', 'frag_keyframe+empty_moov',
|
'-movflags', 'frag_keyframe+empty_moov',
|
||||||
|
@ -418,14 +469,18 @@ class VideoDownload
|
||||||
/**
|
/**
|
||||||
* Get an avconv stream to remux audio and video.
|
* 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
|
* @throws Exception If the popen stream was not created correctly
|
||||||
*
|
*
|
||||||
* @return resource popen stream
|
* @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(
|
$process = new Process(
|
||||||
[
|
[
|
||||||
$this->config->avconv,
|
$this->config->avconv,
|
||||||
|
@ -451,24 +506,24 @@ class VideoDownload
|
||||||
/**
|
/**
|
||||||
* Get video stream from an RTMP video.
|
* 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
|
* @throws Exception If the popen stream was not created correctly
|
||||||
*
|
*
|
||||||
* @return resource popen stream
|
* @return resource popen stream
|
||||||
*/
|
*/
|
||||||
public function getRtmpStream(stdClass $video)
|
public function getRtmpStream()
|
||||||
{
|
{
|
||||||
|
$urls = $this->getUrl();
|
||||||
|
|
||||||
$process = new Process(
|
$process = new Process(
|
||||||
array_merge(
|
array_merge(
|
||||||
[
|
[
|
||||||
$this->config->avconv,
|
$this->config->avconv,
|
||||||
'-v', $this->config->avconvVerbosity,
|
'-v', $this->config->avconvVerbosity,
|
||||||
],
|
],
|
||||||
$this->getRtmpArguments($video),
|
$this->getRtmpArguments(),
|
||||||
[
|
[
|
||||||
'-i', $video->url,
|
'-i', $urls[0],
|
||||||
'-f', $video->ext,
|
'-f', $this->ext,
|
||||||
'pipe:1',
|
'pipe:1',
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@ -481,52 +536,24 @@ class VideoDownload
|
||||||
return $stream;
|
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.
|
* 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 int $audioBitrate Audio bitrate of the converted file
|
||||||
* @param string $filetype Filetype 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 your try to convert and M3U8 video
|
||||||
* @throws Exception If the popen stream was not created correctly
|
* @throws Exception If the popen stream was not created correctly
|
||||||
*
|
*
|
||||||
* @return resource popen stream
|
* @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($this->protocol, ['m3u8', 'm3u8_native'])) {
|
||||||
if (in_array($video->protocol, ['m3u8', 'm3u8_native'])) {
|
|
||||||
throw new Exception(_('Conversion of M3U8 files is not supported.'));
|
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');
|
$stream = popen($avconvProc->getCommandLine(), 'r');
|
||||||
|
|
||||||
|
@ -536,4 +563,31 @@ class VideoDownload
|
||||||
|
|
||||||
return $stream;
|
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]);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -3,7 +3,7 @@
|
||||||
* EmptyUrlException class.
|
* EmptyUrlException class.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace Alltube;
|
namespace Alltube\Exception;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
* PasswordException class.
|
* PasswordException class.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace Alltube;
|
namespace Alltube\Exception;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
33
classes/streams/ConvertedPlaylistArchiveStream.php
Normal file
33
classes/streams/ConvertedPlaylistArchiveStream.php
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* ConvertedPlaylistArchiveStream class.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Alltube\Stream;
|
||||||
|
|
||||||
|
use Alltube\Video;
|
||||||
|
use Slim\Http\Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class used to create a Zip archive from converted playlists entries.
|
||||||
|
*/
|
||||||
|
class ConvertedPlaylistArchiveStream extends PlaylistArchiveStream
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Start streaming a new video.
|
||||||
|
*
|
||||||
|
* @param Video $video Video to stream
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function startVideoStream(Video $video)
|
||||||
|
{
|
||||||
|
$this->curVideoStream = new Stream($video->getAudioStream());
|
||||||
|
|
||||||
|
$this->init_file_stream_transfer(
|
||||||
|
$video->getFileNameWithExtension('mp3'),
|
||||||
|
// The ZIP format does not care about the file size.
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
304
classes/streams/PlaylistArchiveStream.php
Normal file
304
classes/streams/PlaylistArchiveStream.php
Normal file
|
@ -0,0 +1,304 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* PlaylistArchiveStream class.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Alltube\Stream;
|
||||||
|
|
||||||
|
use Alltube\Video;
|
||||||
|
use Barracuda\ArchiveStream\ZipArchive;
|
||||||
|
use Psr\Http\Message\StreamInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class used to create a Zip archive from playlists and stream it to the browser.
|
||||||
|
*
|
||||||
|
* @link https://github.com/php-fig/http-message/blob/master/src/StreamInterface.php
|
||||||
|
*/
|
||||||
|
class PlaylistArchiveStream extends ZipArchive implements StreamInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* videos to add in the archive.
|
||||||
|
*
|
||||||
|
* @var Video[]
|
||||||
|
*/
|
||||||
|
private $videos = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream used to store data before it is sent to the browser.
|
||||||
|
*
|
||||||
|
* @var resource
|
||||||
|
*/
|
||||||
|
private $buffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current video being streamed to the archive.
|
||||||
|
*
|
||||||
|
* @var StreamInterface
|
||||||
|
*/
|
||||||
|
protected $curVideoStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the archive is complete.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
private $isComplete = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PlaylistArchiveStream constructor.
|
||||||
|
*
|
||||||
|
* @param Video $video Video/playlist to download
|
||||||
|
*/
|
||||||
|
public function __construct(Video $video)
|
||||||
|
{
|
||||||
|
$buffer = fopen('php://temp', 'r+');
|
||||||
|
if ($buffer !== false) {
|
||||||
|
$this->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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
196
classes/streams/YoutubeChunkStream.php
Normal file
196
classes/streams/YoutubeChunkStream.php
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* YoutubeChunkStream class.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Alltube\Stream;
|
||||||
|
|
||||||
|
use GuzzleHttp\Psr7\Response;
|
||||||
|
use Psr\Http\Message\StreamInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a wrapper around GuzzleHttp\Psr7\Stream.
|
||||||
|
* It is required because Youtube HTTP responses are buggy if we try to read further than the end of the response.
|
||||||
|
*/
|
||||||
|
class YoutubeChunkStream implements StreamInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* HTTP response containing the video chunk.
|
||||||
|
*
|
||||||
|
* @var Response
|
||||||
|
*/
|
||||||
|
private $response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* YoutubeChunkStream constructor.
|
||||||
|
*
|
||||||
|
* @param Response $response HTTP response containing the video chunk
|
||||||
|
*/
|
||||||
|
public function __construct(Response $response)
|
||||||
|
{
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
}
|
40
classes/streams/YoutubeStream.php
Normal file
40
classes/streams/YoutubeStream.php
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* YoutubeStream class.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Alltube\Stream;
|
||||||
|
|
||||||
|
use Alltube\Video;
|
||||||
|
use GuzzleHttp\Psr7\AppendStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream that downloads a video in chunks.
|
||||||
|
* This is required because Youtube throttles the download speed on chunks larger than 10M.
|
||||||
|
*/
|
||||||
|
class YoutubeStream extends AppendStream
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* YoutubeStream constructor.
|
||||||
|
*
|
||||||
|
* @param Video $video Video to stream
|
||||||
|
*/
|
||||||
|
public function __construct(Video $video)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
$stream = $video->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,7 @@
|
||||||
"homepage": "http://alltubedownload.net/",
|
"homepage": "http://alltubedownload.net/",
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"require": {
|
"require": {
|
||||||
"slim/slim": "~3.11.0",
|
"slim/slim": "~3.12.1",
|
||||||
"mathmarques/smarty-view": "~1.1.0",
|
"mathmarques/smarty-view": "~1.1.0",
|
||||||
"symfony/yaml": "~3.4.1",
|
"symfony/yaml": "~3.4.1",
|
||||||
"symfony/process": "~3.4.1",
|
"symfony/process": "~3.4.1",
|
||||||
|
@ -23,9 +23,10 @@
|
||||||
"phpunit/phpunit": "~6.5.2",
|
"phpunit/phpunit": "~6.5.2",
|
||||||
"doctrine/instantiator": "~1.0.0",
|
"doctrine/instantiator": "~1.0.0",
|
||||||
"ffmpeg/ffmpeg": "4.0.3",
|
"ffmpeg/ffmpeg": "4.0.3",
|
||||||
"rg3/youtube-dl": "2019.01.17",
|
"rg3/youtube-dl": "2019.04.24",
|
||||||
"heroku/heroku-buildpack-php": "*",
|
"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": {
|
"extra": {
|
||||||
"paas": {
|
"paas": {
|
||||||
|
@ -39,10 +40,10 @@
|
||||||
"type": "package",
|
"type": "package",
|
||||||
"package": {
|
"package": {
|
||||||
"name": "rg3/youtube-dl",
|
"name": "rg3/youtube-dl",
|
||||||
"version": "2019.01.17",
|
"version": "2019.04.24",
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"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": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Alltube\\": "classes/",
|
"Alltube\\": "classes/",
|
||||||
|
"Alltube\\Stream\\": "classes/streams/",
|
||||||
|
"Alltube\\Exception\\": "classes/exceptions/",
|
||||||
"Alltube\\Controller\\": "controllers/",
|
"Alltube\\Controller\\": "controllers/",
|
||||||
"Alltube\\Test\\": "tests/"
|
"Alltube\\Test\\": "tests/"
|
||||||
}
|
}
|
||||||
|
|
1128
composer.lock
generated
1128
composer.lock
generated
File diff suppressed because it is too large
Load diff
109
controllers/BaseController.php
Normal file
109
controllers/BaseController.php
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* BaseController class.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Alltube\Controller;
|
||||||
|
|
||||||
|
use Alltube\Config;
|
||||||
|
use Alltube\SessionManager;
|
||||||
|
use Alltube\Video;
|
||||||
|
use Aura\Session\Segment;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
use Slim\Http\Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class used by every controller.
|
||||||
|
*/
|
||||||
|
abstract class BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Current video.
|
||||||
|
*
|
||||||
|
* @var Video
|
||||||
|
*/
|
||||||
|
protected $video;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default youtube-dl format.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $defaultFormat = 'best[protocol=https]/best[protocol=http]';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slim dependency container.
|
||||||
|
*
|
||||||
|
* @var ContainerInterface
|
||||||
|
*/
|
||||||
|
protected $container;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Config instance.
|
||||||
|
*
|
||||||
|
* @var Config
|
||||||
|
*/
|
||||||
|
protected $config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session segment used to store session variables.
|
||||||
|
*
|
||||||
|
* @var Segment
|
||||||
|
*/
|
||||||
|
protected $sessionSegment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BaseController constructor.
|
||||||
|
*
|
||||||
|
* @param ContainerInterface $container Slim dependency container
|
||||||
|
*/
|
||||||
|
public function __construct(ContainerInterface $container)
|
||||||
|
{
|
||||||
|
$this->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;
|
||||||
|
}
|
||||||
|
}
|
281
controllers/DownloadController.php
Normal file
281
controllers/DownloadController.php
Normal file
|
@ -0,0 +1,281 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* DownloadController class.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Alltube\Controller;
|
||||||
|
|
||||||
|
use Alltube\Exception\EmptyUrlException;
|
||||||
|
use Alltube\Exception\PasswordException;
|
||||||
|
use Alltube\Stream\ConvertedPlaylistArchiveStream;
|
||||||
|
use Alltube\Stream\PlaylistArchiveStream;
|
||||||
|
use Alltube\Stream\YoutubeStream;
|
||||||
|
use Alltube\Video;
|
||||||
|
use Exception;
|
||||||
|
use Slim\Http\Request;
|
||||||
|
use Slim\Http\Response;
|
||||||
|
use Slim\Http\Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller that returns a video or audio file.
|
||||||
|
*/
|
||||||
|
class DownloadController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Redirect to video file.
|
||||||
|
*
|
||||||
|
* @param Request $request PSR-7 request
|
||||||
|
* @param Response $response PSR-7 response
|
||||||
|
*
|
||||||
|
* @return Response HTTP response
|
||||||
|
*/
|
||||||
|
public function download(Request $request, Response $response)
|
||||||
|
{
|
||||||
|
$url = $request->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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,55 +6,22 @@
|
||||||
namespace Alltube\Controller;
|
namespace Alltube\Controller;
|
||||||
|
|
||||||
use Alltube\Config;
|
use Alltube\Config;
|
||||||
use Alltube\EmptyUrlException;
|
use Alltube\Exception\PasswordException;
|
||||||
use Alltube\Locale;
|
use Alltube\Locale;
|
||||||
use Alltube\LocaleManager;
|
use Alltube\LocaleManager;
|
||||||
use Alltube\PasswordException;
|
use Alltube\Video;
|
||||||
use Alltube\VideoDownload;
|
|
||||||
use Aura\Session\Segment;
|
|
||||||
use Aura\Session\SessionFactory;
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use GuzzleHttp\Client;
|
|
||||||
use Psr\Container\ContainerInterface;
|
use Psr\Container\ContainerInterface;
|
||||||
use Slim\Container;
|
use Slim\Container;
|
||||||
use Slim\Http\Request;
|
use Slim\Http\Request;
|
||||||
use Slim\Http\Response;
|
use Slim\Http\Response;
|
||||||
use Slim\Http\Stream;
|
|
||||||
use Slim\Views\Smarty;
|
use Slim\Views\Smarty;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main controller.
|
* 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.
|
* Smarty view.
|
||||||
*
|
*
|
||||||
|
@ -62,13 +29,6 @@ class FrontController
|
||||||
*/
|
*/
|
||||||
private $view;
|
private $view;
|
||||||
|
|
||||||
/**
|
|
||||||
* Default youtube-dl format.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
private $defaultFormat = 'best[protocol^=http]';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LocaleManager instance.
|
* LocaleManager instance.
|
||||||
*
|
*
|
||||||
|
@ -77,31 +37,16 @@ class FrontController
|
||||||
private $localeManager;
|
private $localeManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FrontController constructor.
|
* BaseController constructor.
|
||||||
*
|
*
|
||||||
* @param Container $container Slim dependency container
|
* @param ContainerInterface $container Slim dependency container
|
||||||
* @param Config $config Config instance
|
|
||||||
* @param array $cookies Cookie array
|
|
||||||
*/
|
*/
|
||||||
public function __construct(ContainerInterface $container, Config $config = null, array $cookies = [])
|
public function __construct(ContainerInterface $container)
|
||||||
{
|
{
|
||||||
if (isset($config)) {
|
parent::__construct($container);
|
||||||
$this->config = $config;
|
|
||||||
} else {
|
|
||||||
$this->config = Config::getInstance();
|
|
||||||
}
|
|
||||||
$this->download = new VideoDownload($this->config);
|
|
||||||
$this->container = $container;
|
|
||||||
$this->view = $this->container->get('view');
|
|
||||||
$this->localeManager = $this->container->get('locale');
|
$this->localeManager = $this->container->get('locale');
|
||||||
$session_factory = new SessionFactory();
|
$this->view = $this->container->get('view');
|
||||||
$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';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -114,7 +59,7 @@ class FrontController
|
||||||
*/
|
*/
|
||||||
public function index(Request $request, Response $response)
|
public function index(Request $request, Response $response)
|
||||||
{
|
{
|
||||||
$uri = $request->getUri()->withUserInfo(null);
|
$uri = $request->getUri()->withUserInfo('');
|
||||||
$this->view->render(
|
$this->view->render(
|
||||||
$response,
|
$response,
|
||||||
'index.tpl',
|
'index.tpl',
|
||||||
|
@ -163,7 +108,7 @@ class FrontController
|
||||||
'extractors.tpl',
|
'extractors.tpl',
|
||||||
[
|
[
|
||||||
'config' => $this->config,
|
'config' => $this->config,
|
||||||
'extractors' => $this->download->listExtractors(),
|
'extractors' => Video::getExtractors(),
|
||||||
'class' => 'extractors',
|
'class' => 'extractors',
|
||||||
'title' => _('Supported websites'),
|
'title' => _('Supported websites'),
|
||||||
'description' => _('List of all supported websites from which Alltube Download '.
|
'description' => _('List of all supported websites from which Alltube Download '.
|
||||||
|
@ -202,132 +147,45 @@ class FrontController
|
||||||
return $response;
|
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.
|
* Return the video description page.
|
||||||
*
|
*
|
||||||
* @param Request $request PSR-7 request
|
* @param Request $request PSR-7 request
|
||||||
* @param Response $response PSR-7 response
|
* @param Response $response PSR-7 response
|
||||||
* @param array $params GET query parameters
|
|
||||||
* @param string $password Video password
|
|
||||||
*
|
*
|
||||||
* @return Response HTTP response
|
* @return Response HTTP response
|
||||||
*/
|
*/
|
||||||
private function getVideoResponse(Request $request, Response $response, array $params, $password = null)
|
private function getInfoResponse(Request $request, Response $response)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$video = $this->download->getJSON($params['url'], $this->defaultFormat, $password);
|
$this->video->getJson();
|
||||||
} catch (PasswordException $e) {
|
} catch (PasswordException $e) {
|
||||||
return $this->password($request, $response);
|
return $this->password($request, $response);
|
||||||
}
|
}
|
||||||
if ($this->config->stream) {
|
|
||||||
$protocol = '';
|
if (isset($this->video->entries)) {
|
||||||
} else {
|
|
||||||
$protocol = '[protocol^=http]';
|
|
||||||
}
|
|
||||||
if (isset($video->entries)) {
|
|
||||||
$template = 'playlist.tpl';
|
$template = 'playlist.tpl';
|
||||||
} else {
|
} else {
|
||||||
$template = 'video.tpl';
|
$template = 'info.tpl';
|
||||||
}
|
}
|
||||||
$title = _('Video download');
|
$title = _('Video download');
|
||||||
$description = _('Download video from ').$video->extractor_key;
|
$description = _('Download video from ').$this->video->extractor_key;
|
||||||
if (isset($video->title)) {
|
if (isset($this->video->title)) {
|
||||||
$title = $video->title;
|
$title = $this->video->title;
|
||||||
$description = _('Download').' "'.$video->title.'" '._('from').' '.$video->extractor_key;
|
$description = _('Download').' "'.$this->video->title.'" '._('from').' '.$this->video->extractor_key;
|
||||||
}
|
}
|
||||||
$this->view->render(
|
$this->view->render(
|
||||||
$response,
|
$response,
|
||||||
$template,
|
$template,
|
||||||
[
|
[
|
||||||
'video' => $video,
|
'video' => $this->video,
|
||||||
'class' => 'video',
|
'class' => 'info',
|
||||||
'title' => $title,
|
'title' => $title,
|
||||||
'description' => $description,
|
'description' => $description,
|
||||||
'protocol' => $protocol,
|
|
||||||
'config' => $this->config,
|
'config' => $this->config,
|
||||||
'canonical' => $this->getCanonicalUrl($request),
|
'canonical' => $this->getCanonicalUrl($request),
|
||||||
'locale' => $this->localeManager->getLocale(),
|
'locale' => $this->localeManager->getLocale(),
|
||||||
|
'defaultFormat' => $this->defaultFormat,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -342,23 +200,21 @@ class FrontController
|
||||||
*
|
*
|
||||||
* @return Response HTTP response
|
* @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'])) {
|
if (isset($url) && !empty($url)) {
|
||||||
$params['url'] = $params['v'];
|
$this->video = new Video($url, $this->getFormat($request), $this->getPassword($request));
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($params['url']) && !empty($params['url'])) {
|
if ($this->config->convert && $request->getQueryParam('audio')) {
|
||||||
$password = $request->getParam('password');
|
// We skip the info page and get directly to the download.
|
||||||
if (isset($password)) {
|
return $response->withRedirect(
|
||||||
$this->sessionSegment->setFlash($params['url'], $password);
|
$this->container->get('router')->pathFor('download').
|
||||||
}
|
'?'.http_build_query($request->getQueryParams())
|
||||||
if (isset($params['audio'])) {
|
);
|
||||||
return $this->getAudioResponse($request, $response, $params, $password);
|
|
||||||
} else {
|
} else {
|
||||||
return $this->getVideoResponse($request, $response, $params, $password);
|
return $this->getInfoResponse($request, $response);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return $response->withRedirect($this->container->get('router')->pathFor('index'));
|
return $response->withRedirect($this->container->get('router')->pathFor('index'));
|
||||||
|
@ -392,265 +248,6 @@ class FrontController
|
||||||
return $response->withStatus(500);
|
return $response->withStatus(500);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a video/audio stream piped through the server.
|
|
||||||
*
|
|
||||||
* @param string $url URL of the video
|
|
||||||
* @param string $format Requested format
|
|
||||||
* @param Response $response PSR-7 response
|
|
||||||
* @param Request $request PSR-7 request
|
|
||||||
* @param string $password Video password
|
|
||||||
*
|
|
||||||
* @return Response 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.
|
* Generate the canonical URL of the current page.
|
||||||
*
|
*
|
||||||
|
|
44
controllers/JsonController.php
Normal file
44
controllers/JsonController.php
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* JsonController class.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Alltube\Controller;
|
||||||
|
|
||||||
|
use Alltube\Video;
|
||||||
|
use Exception;
|
||||||
|
use Slim\Http\Request;
|
||||||
|
use Slim\Http\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller that returns JSON.
|
||||||
|
*/
|
||||||
|
class JsonController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
{
|
||||||
|
$url = $request->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
537
css/style.css
537
css/style.css
|
@ -1,128 +1,120 @@
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: #EBEBEB;
|
background-color: #ebebeb;
|
||||||
background-image:url('../img/fond.jpg');
|
background-image: url("../img/fond.jpg");
|
||||||
font-family: 'Open Sans', sans-serif;
|
font-family: "Open Sans", sans-serif;
|
||||||
font-weight:400;
|
font-weight: 400;
|
||||||
text-align:center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Header */
|
/* Header */
|
||||||
|
|
||||||
header {
|
header {
|
||||||
padding:0;
|
padding: 0;
|
||||||
position:absolute;
|
position: absolute;
|
||||||
text-align:right;
|
text-align: right;
|
||||||
top:0;
|
top: 0;
|
||||||
width:100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.social {
|
.social {
|
||||||
padding-right:21px;
|
padding-right: 21px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
header .social a {
|
header .social a {
|
||||||
background-position:0 0;
|
background-position: 0 0;
|
||||||
background-repeat:no-repeat;
|
background-repeat: no-repeat;
|
||||||
float:right;
|
float: right;
|
||||||
height:38px;
|
height: 38px;
|
||||||
margin-left:13px;
|
margin-left: 13px;
|
||||||
margin-right:0;
|
margin-right: 0;
|
||||||
margin-top:13px;
|
margin-top: 13px;
|
||||||
overflow:hidden;
|
overflow: hidden;
|
||||||
position:relative;
|
position: relative;
|
||||||
-webkit-transition: all 0.1s ease-in;
|
-webkit-transition: all 0.1s ease-in;
|
||||||
-moz-transition: all 0.1s ease-in;
|
-moz-transition: all 0.1s ease-in;
|
||||||
-o-transition: all 0.1s ease-in;
|
-o-transition: all 0.1s ease-in;
|
||||||
width:38px;
|
width: 38px;
|
||||||
}
|
}
|
||||||
|
|
||||||
header a:focus,
|
header a:focus,
|
||||||
header a:hover {
|
header a:hover {
|
||||||
background-position:0 100%;
|
background-position: 0 100%;
|
||||||
outline:none;
|
outline: none;
|
||||||
-webkit-transition: all 0.1s ease-in;
|
-webkit-transition: all 0.1s ease-in;
|
||||||
-moz-transition: all 0.1s ease-in;
|
-moz-transition: all 0.1s ease-in;
|
||||||
-o-transition: all 0.1s ease-in;
|
-o-transition: all 0.1s ease-in;
|
||||||
}
|
}
|
||||||
|
|
||||||
.share {
|
.share {
|
||||||
background-image:url('../img/share.png');
|
background-image: url("../img/share.png");
|
||||||
}
|
}
|
||||||
|
|
||||||
.sharemask {
|
.sharemask {
|
||||||
background-image:url('../img/sharemask.png');
|
background-image: url("../img/sharemask.png");
|
||||||
background-position:top left;
|
background-position: top left;
|
||||||
background-repeat:no-repeat;
|
background-repeat: no-repeat;
|
||||||
height:38px;
|
height: 38px;
|
||||||
left:0;
|
left: 0;
|
||||||
position:absolute;
|
position: absolute;
|
||||||
top:0;
|
top: 0;
|
||||||
width:38px;
|
width: 38px;
|
||||||
z-index:10;
|
z-index: 10;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.facebook {
|
.facebook {
|
||||||
background-image:url('../img/facebook.png');
|
background-image: url("../img/facebook.png");
|
||||||
}
|
}
|
||||||
|
|
||||||
.facebookmask {
|
.facebookmask {
|
||||||
background-image:url('../img/facebookmask.png');
|
background-image: url("../img/facebookmask.png");
|
||||||
background-position:top left;
|
background-position: top left;
|
||||||
background-repeat:no-repeat;
|
background-repeat: no-repeat;
|
||||||
height:38px;
|
height: 38px;
|
||||||
left:0;
|
left: 0;
|
||||||
position:absolute;
|
position: absolute;
|
||||||
top:0;
|
top: 0;
|
||||||
width:38px;
|
width: 38px;
|
||||||
z-index:10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.twitter {
|
.twitter {
|
||||||
background-image:url('../img/twitter.png');
|
background-image: url("../img/twitter.png");
|
||||||
}
|
}
|
||||||
|
|
||||||
.twittermask {
|
.twittermask {
|
||||||
background-image:url('../img/twittermask.png');
|
background-image: url("../img/twittermask.png");
|
||||||
background-position:top left;
|
background-position: top left;
|
||||||
background-repeat:no-repeat;
|
background-repeat: no-repeat;
|
||||||
height:38px;
|
height: 38px;
|
||||||
left:0;
|
left: 0;
|
||||||
position:absolute;
|
position: absolute;
|
||||||
top:0;
|
top: 0;
|
||||||
width:38px;
|
width: 38px;
|
||||||
z-index:10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Footer */
|
/* Footer */
|
||||||
|
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
background-image:url('../img/fondfooter.png');
|
background-image: url("../img/fondfooter.png");
|
||||||
background-position:top left;
|
background-position: top left;
|
||||||
background-repeat:repeat-x;
|
background-repeat: repeat-x;
|
||||||
bottom:0;
|
bottom: 0;
|
||||||
color:#adadad;
|
color: #adadad;
|
||||||
padding-top:20px;
|
padding-top: 20px;
|
||||||
position:fixed;
|
position: fixed;
|
||||||
text-align:center;
|
text-align: center;
|
||||||
width:100%;
|
width: 100%;
|
||||||
z-index:11;
|
z-index: 11;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer_wrapper {
|
.footer_wrapper {
|
||||||
height:28px;
|
height: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer a{
|
footer a {
|
||||||
color:#adadad;
|
color: #adadad;
|
||||||
text-decoration:none;
|
text-decoration: none;
|
||||||
-webkit-transition: all 0.1s ease-in;
|
-webkit-transition: all 0.1s ease-in;
|
||||||
-moz-transition: all 0.1s ease-in;
|
-moz-transition: all 0.1s ease-in;
|
||||||
-o-transition: all 0.1s ease-in;
|
-o-transition: all 0.1s ease-in;
|
||||||
|
@ -130,47 +122,42 @@ footer a{
|
||||||
|
|
||||||
footer a:focus,
|
footer a:focus,
|
||||||
footer a:hover {
|
footer a:hover {
|
||||||
color:#f2084a;
|
color: #f2084a;
|
||||||
outline:none;
|
outline: none;
|
||||||
-webkit-transition: all 0.1s ease-in;
|
-webkit-transition: all 0.1s ease-in;
|
||||||
-moz-transition: all 0.1s ease-in;
|
-moz-transition: all 0.1s ease-in;
|
||||||
-o-transition: all 0.1s ease-in;
|
-o-transition: all 0.1s ease-in;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Home content */
|
/* Home content */
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
padding-bottom:55px;
|
padding-bottom: 55px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.labelurl {
|
.labelurl {
|
||||||
color:#3f3f3f;
|
color: #3f3f3f;
|
||||||
font-size:19px;
|
font-size: 19px;
|
||||||
position:relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.champs {
|
.champs {
|
||||||
margin-bottom:70px;
|
margin-bottom: 70px;
|
||||||
margin-top:8px;
|
margin-top: 8px;
|
||||||
position:relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.downloadBtn {
|
.downloadBtn {
|
||||||
background-color:#3A3A3A;
|
background-color: #3a3a3a;
|
||||||
border: 3px solid #a5a5a5;
|
border: 3px solid #a5a5a5;
|
||||||
border-radius:10px;
|
border-radius: 10px;
|
||||||
color:#dedede;
|
color: #dedede;
|
||||||
cursor:pointer;
|
cursor: pointer;
|
||||||
display:inline-block;
|
display: inline-block;
|
||||||
font-weight:800;
|
font-weight: 800;
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
position:relative;
|
position: relative;
|
||||||
text-decoration:none;
|
text-decoration: none;
|
||||||
-webkit-transition: all 0.1s ease-in;
|
-webkit-transition: all 0.1s ease-in;
|
||||||
-moz-transition: all 0.1s ease-in;
|
-moz-transition: all 0.1s ease-in;
|
||||||
-o-transition: all 0.1s ease-in;
|
-o-transition: all 0.1s ease-in;
|
||||||
|
@ -178,63 +165,62 @@ footer a:hover {
|
||||||
|
|
||||||
.downloadBtn:focus,
|
.downloadBtn:focus,
|
||||||
.downloadBtn:hover {
|
.downloadBtn:hover {
|
||||||
background-color:#f2084a;
|
background-color: #f2084a;
|
||||||
outline:none;
|
outline: none;
|
||||||
-webkit-transition: all 0.1s ease-in;
|
-webkit-transition: all 0.1s ease-in;
|
||||||
-moz-transition: all 0.1s ease-in;
|
-moz-transition: all 0.1s ease-in;
|
||||||
-o-transition: all 0.1s ease-in;
|
-o-transition: all 0.1s ease-in;
|
||||||
}
|
}
|
||||||
|
|
||||||
.downloadBtn::-moz-focus-inner {
|
.downloadBtn::-moz-focus-inner {
|
||||||
border:none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.URLinput{
|
.URLinput {
|
||||||
background-color:#fff;
|
background-color: #fff;
|
||||||
border: 3px solid #a5a5a5;
|
border: 3px solid #a5a5a5;
|
||||||
border-radius:10px;
|
border-radius: 10px;
|
||||||
color:#3F3F3F;
|
color: #3f3f3f;
|
||||||
font-weight:800;
|
font-weight: 800;
|
||||||
margin-right:8px;
|
margin-right: 8px;
|
||||||
min-width:426px;
|
min-width: 426px;
|
||||||
padding: 12px 12px 12px 12px;
|
padding: 12px 12px 12px 12px;
|
||||||
position:relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.URLinput:focus {
|
.URLinput:focus {
|
||||||
border-color:#3A3A3A;
|
border-color: #3a3a3a;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.URLinput:-webkit-input-placeholder{
|
.URLinput:-webkit-input-placeholder {
|
||||||
color:#c1cfcf;
|
color: #c1cfcf;
|
||||||
}
|
}
|
||||||
.URLinput:-moz-placeholder {
|
.URLinput:-moz-placeholder {
|
||||||
color:#c1cfcf;
|
color: #c1cfcf;
|
||||||
}
|
}
|
||||||
|
|
||||||
.combatiblelink {
|
.combatiblelink {
|
||||||
background-image:url('../img/compatiblerouage.png');
|
background-image: url("../img/compatiblerouage.png");
|
||||||
background-position:0 100%;
|
background-position: 0 100%;
|
||||||
background-repeat:no-repeat;
|
background-repeat: no-repeat;
|
||||||
color:#a5a5a5;
|
color: #a5a5a5;
|
||||||
padding-bottom:10px;
|
padding-bottom: 10px;
|
||||||
padding-left:41px;
|
padding-left: 41px;
|
||||||
padding-top:10px;
|
padding-top: 10px;
|
||||||
position:relative;
|
position: relative;
|
||||||
text-decoration:none;
|
text-decoration: none;
|
||||||
-webkit-transition: all 0.1s ease-in;
|
-webkit-transition: all 0.1s ease-in;
|
||||||
-moz-transition: all 0.1s ease-in;
|
-moz-transition: all 0.1s ease-in;
|
||||||
-o-transition: all 0.1s ease-in;
|
-o-transition: all 0.1s ease-in;
|
||||||
z-index:10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.combatiblelink:focus,
|
.combatiblelink:focus,
|
||||||
.combatiblelink:hover {
|
.combatiblelink:hover {
|
||||||
background-position:0 0;
|
background-position: 0 0;
|
||||||
color:#f2084a;
|
color: #f2084a;
|
||||||
outline:none;
|
outline: none;
|
||||||
-webkit-transition: all 0.1s ease-in;
|
-webkit-transition: all 0.1s ease-in;
|
||||||
-moz-transition: all 0.1s ease-in;
|
-moz-transition: all 0.1s ease-in;
|
||||||
-o-transition: all 0.1s ease-in;
|
-o-transition: all 0.1s ease-in;
|
||||||
|
@ -242,26 +228,26 @@ footer a:hover {
|
||||||
|
|
||||||
.bookmarklet {
|
.bookmarklet {
|
||||||
border: 2px dotted;
|
border: 2px dotted;
|
||||||
color:gray;
|
color: gray;
|
||||||
padding:10px 30px;
|
padding: 10px 30px;
|
||||||
position:relative;
|
position: relative;
|
||||||
text-decoration:none;
|
text-decoration: none;
|
||||||
z-index:10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mp3 {
|
.mp3 {
|
||||||
background-color:#cecece;
|
background-color: #cecece;
|
||||||
border-radius:6px;
|
border-radius: 6px;
|
||||||
color:#3f3f3f;
|
color: #3f3f3f;
|
||||||
height:26px;
|
height: 26px;
|
||||||
margin-top:12px;
|
margin-top: 12px;
|
||||||
position:relative;
|
position: relative;
|
||||||
text-align:left;
|
text-align: left;
|
||||||
width:622px;
|
width: 622px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mp3-inner {
|
.mp3-inner {
|
||||||
padding:3px;
|
padding: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.audio:not(:checked),
|
.audio:not(:checked),
|
||||||
|
@ -272,7 +258,7 @@ footer a:hover {
|
||||||
.audio:not(:checked) + label,
|
.audio:not(:checked) + label,
|
||||||
.audio:checked + label {
|
.audio:checked + label {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
line-height:20px;
|
line-height: 20px;
|
||||||
padding-left: 82px;
|
padding-left: 82px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
@ -280,7 +266,7 @@ footer a:hover {
|
||||||
.audio:checked + label:before,
|
.audio:checked + label:before,
|
||||||
.audio:not(:checked) + label:after,
|
.audio:not(:checked) + label:after,
|
||||||
.audio:checked + label:after {
|
.audio:checked + label:after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
.audio:not(:checked) + label:before,
|
.audio:not(:checked) + label:before,
|
||||||
|
@ -288,13 +274,13 @@ footer a:hover {
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
left:0;
|
left: 0;
|
||||||
top: -1px;
|
top: -1px;
|
||||||
-webkit-transition: background-color .2s;
|
-webkit-transition: background-color 0.2s;
|
||||||
-moz-transition: background-color .2s;
|
-moz-transition: background-color 0.2s;
|
||||||
-ms-transition: background-color .2s;
|
-ms-transition: background-color 0.2s;
|
||||||
-o-transition: background-color .2s;
|
-o-transition: background-color 0.2s;
|
||||||
transition: background-color .2s;
|
transition: background-color 0.2s;
|
||||||
width: 45px;
|
width: 45px;
|
||||||
}
|
}
|
||||||
.audio:not(:checked) + label:after,
|
.audio:not(:checked) + label:after,
|
||||||
|
@ -304,21 +290,21 @@ footer a:hover {
|
||||||
height: 16px;
|
height: 16px;
|
||||||
left: 2px;
|
left: 2px;
|
||||||
top: 1px;
|
top: 1px;
|
||||||
-webkit-transition: all .2s;
|
-webkit-transition: all 0.2s;
|
||||||
-moz-transition: all .2s;
|
-moz-transition: all 0.2s;
|
||||||
-ms-transition: all .2s;
|
-ms-transition: all 0.2s;
|
||||||
-o-transition: all .2s;
|
-o-transition: all 0.2s;
|
||||||
transition: all .2s;
|
transition: all 0.2s;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.audio:focus + label {
|
.audio:focus + label {
|
||||||
color:black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* on checked */
|
/* on checked */
|
||||||
.audio:checked + label:before {
|
.audio:checked + label:before {
|
||||||
background:#f2084a;
|
background: #f2084a;
|
||||||
}
|
}
|
||||||
.audio:checked + label:after {
|
.audio:checked + label:after {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
@ -332,44 +318,44 @@ footer a:hover {
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
height:20px;
|
height: 20px;
|
||||||
left: 3px;
|
left: 3px;
|
||||||
line-height: 17px;
|
line-height: 17px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
-webkit-transition: all .2s;
|
-webkit-transition: all 0.2s;
|
||||||
-moz-transition: all .2s;
|
-moz-transition: all 0.2s;
|
||||||
-ms-transition: all .2s;
|
-ms-transition: all 0.2s;
|
||||||
-o-transition: all .2s;
|
-o-transition: all 0.2s;
|
||||||
transition: all .2s;
|
transition: all 0.2s;
|
||||||
width: 45px;
|
width: 45px;
|
||||||
}
|
}
|
||||||
.audio:not(:checked) + label .ui:before {
|
.audio:not(:checked) + label .ui:before {
|
||||||
background-image:url('../img/mp3hover.png');
|
background-image: url("../img/mp3hover.png");
|
||||||
background-position:right top;
|
background-position: right top;
|
||||||
background-repeat:no-repeat;
|
background-repeat: no-repeat;
|
||||||
content: "no";
|
content: "no";
|
||||||
left: 0;
|
left: 0;
|
||||||
min-width:56px;
|
min-width: 56px;
|
||||||
padding-left:23px;
|
padding-left: 23px;
|
||||||
padding-top:2px;
|
padding-top: 2px;
|
||||||
-webkit-transition: all .2s;
|
-webkit-transition: all 0.2s;
|
||||||
-moz-transition: all .2s;
|
-moz-transition: all 0.2s;
|
||||||
-ms-transition: all .2s;
|
-ms-transition: all 0.2s;
|
||||||
-o-transition: all .2s;
|
-o-transition: all 0.2s;
|
||||||
transition: all .2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
.audio:checked + label .ui:after {
|
.audio:checked + label .ui:after {
|
||||||
background-image:url('../img/mp3.png');
|
background-image: url("../img/mp3.png");
|
||||||
background-position:right top;
|
background-position: right top;
|
||||||
background-repeat:no-repeat;
|
background-repeat: no-repeat;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
content: "yes";
|
content: "yes";
|
||||||
-webkit-transition: all .2s;
|
-webkit-transition: all 0.2s;
|
||||||
-moz-transition: all .2s;
|
-moz-transition: all 0.2s;
|
||||||
-ms-transition: all .2s;
|
-ms-transition: all 0.2s;
|
||||||
-o-transition: all .2s;
|
-o-transition: all 0.2s;
|
||||||
transition: all .2s;
|
transition: all 0.2s;
|
||||||
width:73px;
|
width: 73px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.seekOptions {
|
.seekOptions {
|
||||||
|
@ -382,7 +368,6 @@ footer a:hover {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Playlists */
|
/* Playlists */
|
||||||
.playlist-entry .thumb {
|
.playlist-entry .thumb {
|
||||||
float: left;
|
float: left;
|
||||||
|
@ -413,126 +398,120 @@ footer a:hover {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Supported websites list */
|
/* Supported websites list */
|
||||||
|
|
||||||
.logobis {
|
.logobis {
|
||||||
height:107px;
|
height: 107px;
|
||||||
margin:0 auto 10px auto;
|
margin: 0 auto 10px auto;
|
||||||
position:relative;
|
position: relative;
|
||||||
width:447px;
|
width: 447px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.logocompatible {
|
.logocompatible {
|
||||||
background-image:url('../img/logocompatible.png');
|
background-image: url("../img/logocompatible.png");
|
||||||
background-position:0 0;
|
background-position: 0 0;
|
||||||
background-repeat:repeat-y;
|
background-repeat: repeat-y;
|
||||||
display:block;
|
display: block;
|
||||||
height:107px;
|
height: 107px;
|
||||||
-webkit-transition: all 0.1s ease-in;
|
-webkit-transition: all 0.1s ease-in;
|
||||||
-moz-transition: all 0.1s ease-in;
|
-moz-transition: all 0.1s ease-in;
|
||||||
-o-transition: all 0.1s ease-in;
|
-o-transition: all 0.1s ease-in;
|
||||||
width:447px;
|
width: 447px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logocompatible:focus,
|
.logocompatible:focus,
|
||||||
.logocompatible:hover {
|
.logocompatible:hover {
|
||||||
background-position:0 100%;
|
background-position: 0 100%;
|
||||||
outline:none;
|
outline: none;
|
||||||
-webkit-transition: all 0.1s ease-in;
|
-webkit-transition: all 0.1s ease-in;
|
||||||
-moz-transition: all 0.1s ease-in;
|
-moz-transition: all 0.1s ease-in;
|
||||||
-o-transition: all 0.1s ease-in;
|
-o-transition: all 0.1s ease-in;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.logocompatiblemask {
|
.logocompatiblemask {
|
||||||
background-image:url('../img/logocompatiblemask.png');
|
background-image: url("../img/logocompatiblemask.png");
|
||||||
background-position:0 100%;
|
background-position: 0 100%;
|
||||||
background-repeat:no-repeat;
|
background-repeat: no-repeat;
|
||||||
height:107px;
|
height: 107px;
|
||||||
left:0;
|
left: 0;
|
||||||
position:absolute;
|
position: absolute;
|
||||||
top:0;
|
top: 0;
|
||||||
width:447px;
|
width: 447px;
|
||||||
z-index:10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.titre {
|
.titre {
|
||||||
color:#383838;
|
color: #383838;
|
||||||
font-family: 'Open Sans', sans-serif;
|
font-family: "Open Sans", sans-serif;
|
||||||
font-size:48px;
|
font-size: 48px;
|
||||||
font-weight:300;
|
font-weight: 300;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tripleliste {
|
.tripleliste {
|
||||||
margin-left:auto;
|
margin-left: auto;
|
||||||
margin-right:auto;
|
margin-right: auto;
|
||||||
margin-top:80px;
|
margin-top: 80px;
|
||||||
position:relative;
|
position: relative;
|
||||||
width:800px;
|
width: 800px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.tripleliste ul {
|
.tripleliste ul {
|
||||||
margin-bottom:1em;
|
margin-bottom: 1em;
|
||||||
margin-left:120px;
|
margin-left: 120px;
|
||||||
width:600px;
|
width: 600px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tripleliste ul li {
|
.tripleliste ul li {
|
||||||
color:#383838;
|
color: #383838;
|
||||||
float:left;
|
float: left;
|
||||||
list-style-type:none;
|
list-style-type: none;
|
||||||
position:relative;
|
position: relative;
|
||||||
text-align:left;
|
text-align: left;
|
||||||
width:200px;
|
width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
height:100%;
|
height: 100%;
|
||||||
margin:0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
.wrapper {
|
.wrapper {
|
||||||
-moz-box-sizing: border-box;
|
-moz-box-sizing: border-box;
|
||||||
-webkit-box-sizing: border-box;
|
-webkit-box-sizing: border-box;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display:table;
|
display: table;
|
||||||
height:100%;
|
height: 100%;
|
||||||
margin:auto;
|
margin: auto;
|
||||||
padding-bottom:110px;
|
padding-bottom: 110px;
|
||||||
}
|
}
|
||||||
.main {
|
.main {
|
||||||
display:table-cell;
|
display: table-cell;
|
||||||
vertical-align:middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.extractors {
|
.extractors {
|
||||||
padding-top:60px;
|
padding-top: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.extractors .wrapper {
|
.extractors .wrapper {
|
||||||
padding-bottom:5em;
|
padding-bottom: 5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logocompatible,
|
.logocompatible,
|
||||||
.social a {
|
.social a {
|
||||||
font-size:0;
|
font-size: 0;
|
||||||
text-decoration:none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.social a {
|
.social a {
|
||||||
color:#D1D1D1;
|
color: #d1d1d1;
|
||||||
}
|
}
|
||||||
.logocompatible {
|
.logocompatible {
|
||||||
color: #4F4F4F
|
color: #4f4f4f;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
margin:0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
|
@ -540,19 +519,19 @@ h1 {
|
||||||
}
|
}
|
||||||
|
|
||||||
.error p {
|
.error p {
|
||||||
text-align:justify;
|
text-align: justify;
|
||||||
}
|
}
|
||||||
|
|
||||||
.smaller {
|
.smaller {
|
||||||
font-size:smaller;
|
font-size: smaller;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumb {
|
.thumb {
|
||||||
max-width:700px;
|
max-width: 700px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.format {
|
.format {
|
||||||
text-align:left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.best {
|
.best {
|
||||||
|
@ -560,7 +539,7 @@ h1 {
|
||||||
}
|
}
|
||||||
|
|
||||||
.monospace {
|
.monospace {
|
||||||
font-family:monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customBitrate {
|
.customBitrate {
|
||||||
|
@ -591,7 +570,7 @@ h1 {
|
||||||
}
|
}
|
||||||
|
|
||||||
.supportedLocales li {
|
.supportedLocales li {
|
||||||
border-bottom: thin solid #E1E1E1;
|
border-bottom: thin solid #e1e1e1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.supportedLocales li:last-child {
|
.supportedLocales li:last-child {
|
||||||
|
@ -629,20 +608,20 @@ h1 {
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.formats,
|
.formats,
|
||||||
.thumb {
|
.thumb {
|
||||||
width:90%;
|
width: 90%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.URLinput{
|
.URLinput {
|
||||||
min-width:0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
max-width:330px;
|
max-width: 330px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logocompatible,
|
.logocompatible,
|
||||||
.logocompatible img {
|
.logocompatible img {
|
||||||
max-width:447px;
|
max-width: 447px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logocompatible,
|
.logocompatible,
|
||||||
|
@ -650,18 +629,18 @@ h1 {
|
||||||
.champs,
|
.champs,
|
||||||
.URLinput,
|
.URLinput,
|
||||||
.mp3 {
|
.mp3 {
|
||||||
height:auto;
|
height: auto;
|
||||||
margin:auto;
|
margin: auto;
|
||||||
width:90%;
|
width: 90%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
margin-top:50px;
|
margin-top: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logocompatible img {
|
.logocompatible img {
|
||||||
height: auto;
|
height: auto;
|
||||||
width:100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.downloadBtn {
|
.downloadBtn {
|
||||||
|
@ -672,46 +651,46 @@ h1 {
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
display:none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tripleliste ul,
|
.tripleliste ul,
|
||||||
.tripleliste {
|
.tripleliste {
|
||||||
margin-left:auto;
|
margin-left: auto;
|
||||||
margin-top:auto;
|
margin-top: auto;
|
||||||
width:auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logocompatiblemask {
|
.logocompatiblemask {
|
||||||
background:none;
|
background: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logocompatible {
|
.logocompatible {
|
||||||
background-color:#4F4F4F;
|
background-color: #4f4f4f;
|
||||||
background-image:none;
|
background-image: none;
|
||||||
height:auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logocompatiblemask,
|
.logocompatiblemask,
|
||||||
.logobis {
|
.logobis {
|
||||||
width:auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logocompatiblemask {
|
.logocompatiblemask {
|
||||||
position:static;
|
position: static;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logobis {
|
.logobis {
|
||||||
height:auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.titre {
|
.titre {
|
||||||
margin:auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error p {
|
.error p {
|
||||||
padding:0.5em;
|
padding: 0.5em;
|
||||||
text-align:left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.playlist-entry {
|
.playlist-entry {
|
||||||
|
@ -723,7 +702,6 @@ h1 {
|
||||||
float: none;
|
float: none;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media all and (display-mode: standalone) {
|
@media all and (display-mode: standalone) {
|
||||||
|
@ -731,3 +709,16 @@ h1 {
|
||||||
display: none;
|
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;
|
||||||
|
}
|
||||||
|
|
55
index.php
55
index.php
|
@ -2,10 +2,11 @@
|
||||||
|
|
||||||
require_once __DIR__.'/vendor/autoload.php';
|
require_once __DIR__.'/vendor/autoload.php';
|
||||||
use Alltube\Config;
|
use Alltube\Config;
|
||||||
|
use Alltube\Controller\DownloadController;
|
||||||
use Alltube\Controller\FrontController;
|
use Alltube\Controller\FrontController;
|
||||||
|
use Alltube\Controller\JsonController;
|
||||||
use Alltube\LocaleManager;
|
use Alltube\LocaleManager;
|
||||||
use Alltube\LocaleMiddleware;
|
use Alltube\LocaleMiddleware;
|
||||||
use Alltube\PlaylistArchiveStream;
|
|
||||||
use Alltube\UglyRouter;
|
use Alltube\UglyRouter;
|
||||||
use Alltube\ViewFactory;
|
use Alltube\ViewFactory;
|
||||||
use Slim\App;
|
use Slim\App;
|
||||||
|
@ -15,7 +16,9 @@ if (isset($_SERVER['REQUEST_URI']) && strpos($_SERVER['REQUEST_URI'], '/index.ph
|
||||||
die;
|
die;
|
||||||
}
|
}
|
||||||
|
|
||||||
stream_wrapper_register('playlist', PlaylistArchiveStream::class);
|
if (is_file(__DIR__.'/config/config.yml')) {
|
||||||
|
Config::setFile(__DIR__.'/config/config.yml');
|
||||||
|
}
|
||||||
|
|
||||||
$app = new App();
|
$app = new App();
|
||||||
$container = $app->getContainer();
|
$container = $app->getContainer();
|
||||||
|
@ -28,42 +31,54 @@ $container['view'] = ViewFactory::create($container);
|
||||||
if (!class_exists('Locale')) {
|
if (!class_exists('Locale')) {
|
||||||
die('You need to install the intl extension for PHP.');
|
die('You need to install the intl extension for PHP.');
|
||||||
}
|
}
|
||||||
$container['locale'] = new LocaleManager($_COOKIE);
|
$container['locale'] = new LocaleManager();
|
||||||
$app->add(new LocaleMiddleware($container));
|
$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(
|
$app->get(
|
||||||
'/',
|
'/',
|
||||||
[$controller, 'index']
|
[$frontController, 'index']
|
||||||
)->setName('index');
|
)->setName('index');
|
||||||
|
|
||||||
$app->get(
|
$app->get(
|
||||||
'/extractors',
|
'/extractors',
|
||||||
[$controller, 'extractors']
|
[$frontController, 'extractors']
|
||||||
)->setName('extractors');
|
)->setName('extractors');
|
||||||
|
|
||||||
$app->any(
|
$app->any(
|
||||||
'/video',
|
'/info',
|
||||||
[$controller, 'video']
|
[$frontController, 'info']
|
||||||
)->setName('video');
|
)->setName('info');
|
||||||
|
// Legacy route.
|
||||||
|
$app->any('/video', [$frontController, 'info']);
|
||||||
|
|
||||||
$app->any(
|
$app->any(
|
||||||
'/watch',
|
'/watch',
|
||||||
[$controller, 'video']
|
[$frontController, 'video']
|
||||||
);
|
);
|
||||||
$app->get(
|
|
||||||
'/redirect',
|
$app->any(
|
||||||
[$controller, 'redirect']
|
'/download',
|
||||||
)->setName('redirect');
|
[$downloadController, 'download']
|
||||||
$app->get(
|
)->setName('download');
|
||||||
'/json',
|
// Legacy route.
|
||||||
[$controller, 'json']
|
$app->get('/redirect', [$downloadController, 'download']);
|
||||||
)->setName('json');
|
|
||||||
$app->get(
|
$app->get(
|
||||||
'/locale/{locale}',
|
'/locale/{locale}',
|
||||||
[$controller, 'locale']
|
[$frontController, 'locale']
|
||||||
)->setName('locale');
|
)->setName('locale');
|
||||||
|
|
||||||
|
$app->get(
|
||||||
|
'/json',
|
||||||
|
[$jsonController, 'json']
|
||||||
|
)->setName('json');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$app->run();
|
$app->run();
|
||||||
} catch (SmartyException $e) {
|
} catch (SmartyException $e) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "alltube",
|
"name": "alltube",
|
||||||
"description": "HTML GUI for youtube-dl",
|
"description": "HTML GUI for youtube-dl",
|
||||||
"version": "1.2.5",
|
"version": "2.0.0",
|
||||||
"author": "Pierre Rudloff",
|
"author": "Pierre Rudloff",
|
||||||
"bugs": "https://github.com/Rudloff/alltube/issues",
|
"bugs": "https://github.com/Rudloff/alltube/issues",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -21,6 +21,7 @@
|
||||||
"grunt-markdownlint": "~2.1.0",
|
"grunt-markdownlint": "~2.1.0",
|
||||||
"grunt-phpcs": "~0.4.0",
|
"grunt-phpcs": "~0.4.0",
|
||||||
"grunt-phpdocumentor": "~0.4.1",
|
"grunt-phpdocumentor": "~0.4.1",
|
||||||
|
"grunt-phpstan": "~0.2.0",
|
||||||
"grunt-phpunit": "~0.3.6"
|
"grunt-phpunit": "~0.3.6"
|
||||||
},
|
},
|
||||||
"homepage": "https://www.alltubedownload.net/",
|
"homepage": "https://www.alltubedownload.net/",
|
||||||
|
|
4
phpstan.neon
Normal file
4
phpstan.neon
Normal file
|
@ -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\.#'
|
|
@ -1,25 +1,25 @@
|
||||||
{locale path="../i18n" domain="Alltube"}
|
{locale path="../i18n" domain="Alltube"}
|
||||||
<!Doctype HTML>
|
<!doctype html>
|
||||||
<html {if isset($locale)}lang="{$locale->getBcp47()}"{/if}>
|
<html {if isset($locale)}lang="{$locale->getBcp47()}"{/if}>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name=viewport content="width=device-width, initial-scale=1">
|
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||||
{if isset($description)}
|
{if isset($description)}
|
||||||
<meta name="description" content="{$description|escape}" />
|
<meta name="description" content="{$description|escape}" />
|
||||||
<meta name="twitter:description" content="{$description|escape}" />
|
<meta name="twitter:description" content="{$description|escape}" />
|
||||||
<meta property="og:description" content="{$description|escape}" />
|
<meta property="og:description" content="{$description|escape}" />
|
||||||
{/if}
|
{/if}
|
||||||
<link rel="stylesheet" href="{base_url}/dist/main.css" />
|
<link rel="stylesheet" href="{base_url}/dist/main.css" />
|
||||||
<title>{$config->appName}{if isset($title)} - {$title|escape}{/if}</title>
|
<title>{$config->appName}{if isset($title)} - {$title|escape}{/if}</title>
|
||||||
<link rel="canonical" href="{$canonical}" />
|
<link rel="canonical" href="{$canonical}" />
|
||||||
<link rel="icon" href="{base_url}/img/favicon.png" />
|
<link rel="icon" href="{base_url}/img/favicon.png" />
|
||||||
<meta property="og:title" content="{$config->appName}{if isset($title)} - {$title|escape}{/if}" />
|
<meta property="og:title" content="{$config->appName}{if isset($title)} - {$title|escape}{/if}" />
|
||||||
<meta property="og:image" content="{base_url}/img/logo.png" />
|
<meta property="og:image" content="{base_url}/img/logo.png" />
|
||||||
<meta name="twitter:card" content="summary" />
|
<meta name="twitter:card" content="summary" />
|
||||||
<meta name="twitter:title" content="{$config->appName}{if isset($title)} - {$title|escape}{/if}" />
|
<meta name="twitter:title" content="{$config->appName}{if isset($title)} - {$title|escape}{/if}" />
|
||||||
<meta name="twitter:image" content="{base_url}/img/logo.png" />
|
<meta name="twitter:image" content="{base_url}/img/logo.png" />
|
||||||
<meta name="twitter:creator" content="@Tael67" />
|
<meta name="twitter:creator" content="@Tael67" />
|
||||||
<meta name="theme-color" content="#4F4F4F">
|
<meta name="theme-color" content="#4F4F4F">
|
||||||
<link rel="manifest" href="{base_url}/resources/manifest.json" />
|
<link rel="manifest" href="{base_url}/resources/manifest.json" />
|
||||||
</head>
|
</head>
|
||||||
<body class="{$class}">
|
<body class="{$class}">
|
||||||
|
|
|
@ -18,9 +18,12 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="social">
|
<div class="social">
|
||||||
<a class="twitter" rel="noopener" href="http://twitter.com/home?status={base_url|urlencode}" title="{t}Share on Twitter{/t}" target="_blank">
|
<a class="twitter" rel="noopener" href="http://twitter.com/home?status={base_url|urlencode}" title="{t}Share on Twitter{/t}" target="_blank" aria-label="{t}Share on Twitter{/t} {t}(opens a new window){/t}">
|
||||||
<div class="twittermask"></div></a>
|
<div class="twittermask"></div>
|
||||||
<a class="facebook" rel="noopener" href="https://www.facebook.com/sharer/sharer.php?u={base_url|urlencode}" title="{t}Share on Facebook{/t}" target="_blank"><div class="facebookmask"></div></a>
|
</a>
|
||||||
|
<a class="facebook" rel="noopener" href="https://www.facebook.com/sharer/sharer.php?u={base_url|urlencode}" title="{t}Share on Facebook{/t}" target="_blank" aria-label="{t}Share on Facebook{/t} {t}(opens a new window){/t}">
|
||||||
|
<div class="facebookmask"></div>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<main class="main">
|
<main class="main">
|
||||||
<div><img class="logo" src="{base_url}/img/logo.png"
|
<div><img class="logo" src="{base_url}/img/logo.png"
|
||||||
alt="{$config->appName}" width="328" height="284"></div>
|
alt="{$config->appName}" width="328" height="284"></div>
|
||||||
<form action="{path_for name="video"}">
|
<form action="{path_for name="info"}">
|
||||||
<label class="labelurl" for="url">
|
<label class="labelurl" for="url">
|
||||||
{t}Copy here the URL of your video (Youtube, Dailymotion, etc.){/t}
|
{t}Copy here the URL of your video (Youtube, Dailymotion, etc.){/t}
|
||||||
</label>
|
</label>
|
||||||
|
@ -14,7 +14,7 @@
|
||||||
required placeholder="http://example.com/video" />
|
required placeholder="http://example.com/video" />
|
||||||
</span>
|
</span>
|
||||||
{if $config->uglyUrls}
|
{if $config->uglyUrls}
|
||||||
<input type="hidden" name="page" value="video" />
|
<input type="hidden" name="page" value="info" />
|
||||||
{/if}
|
{/if}
|
||||||
<input class="downloadBtn large-font" type="submit" value="{t}Download{/t}" /><br/>
|
<input class="downloadBtn large-font" type="submit" value="{t}Download{/t}" /><br/>
|
||||||
{if $config->convert}
|
{if $config->convert}
|
||||||
|
@ -25,8 +25,8 @@
|
||||||
{t}Audio only (MP3){/t}
|
{t}Audio only (MP3){/t}
|
||||||
</label>
|
</label>
|
||||||
<div class="seekOptions">
|
<div class="seekOptions">
|
||||||
{t}From{/t} <input type="text" pattern="(\d+:)?(\d+:)?\d+(\.\d+)?" placeholder="HH:MM:SS" value="" name="from"/>
|
<label for="from">{t}From{/t}</label> <input type="text" pattern="(\d+:)?(\d+:)?\d+(\.\d+)?" placeholder="HH:MM:SS" value="" name="from" id="from" />
|
||||||
{t}to{/t} <input type="text" pattern="(\d+:)?(\d+:)?\d+(\.\d+)?" placeholder="HH:MM:SS" value="" name="to"/>
|
<label for="to">{t}to{/t}</label> <input type="text" pattern="(\d+:)?(\d+:)?\d+(\.\d+)?" placeholder="HH:MM:SS" value="" name="to" id="to" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
<a class="combatiblelink small-font" href="{path_for name="extractors"}">{t}See all supported websites{/t}</a>
|
<a class="combatiblelink small-font" href="{path_for name="extractors"}">{t}See all supported websites{/t}</a>
|
||||||
<div id="bookmarklet" class="bookmarklet_wrapper">
|
<div id="bookmarklet" class="bookmarklet_wrapper">
|
||||||
<p> {t}Drag this to your bookmarks bar:{/t} </p>
|
<p> {t}Drag this to your bookmarks bar:{/t} </p>
|
||||||
<a class="bookmarklet small-font" href="javascript:window.location='{$domain}{path_for name='video'}?url='+encodeURIComponent(location.href);">{t}Bookmarklet{/t}</a>
|
<a class="bookmarklet small-font" href="javascript:window.location='{$domain}{path_for name='info'}?url='+encodeURIComponent(location.href);">{t}Bookmarklet{/t}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
|
@ -18,16 +18,16 @@
|
||||||
<meta itemprop="uploadDate" content="{$video->upload_date}" />
|
<meta itemprop="uploadDate" content="{$video->upload_date}" />
|
||||||
{/if}
|
{/if}
|
||||||
<br/>
|
<br/>
|
||||||
<form action="{path_for name="redirect"}">
|
<form action="{path_for name="download"}">
|
||||||
<input type="hidden" name="url" value="{$video->webpage_url}" />
|
<input type="hidden" name="url" value="{$video->webpage_url}" />
|
||||||
{if isset($video->formats)}
|
{if isset($video->formats)}
|
||||||
<h3><label for="format">{t}Available formats:{/t}</label></h3>
|
<h3><label for="format">{t}Available formats:{/t}</label></h3>
|
||||||
{if $config->uglyUrls}
|
{if $config->uglyUrls}
|
||||||
<input type="hidden" name="page" value="redirect" />
|
<input type="hidden" name="page" value="download" />
|
||||||
{/if}
|
{/if}
|
||||||
<select name="format" id="format" class="formats monospace">
|
<select name="format" id="format" class="formats monospace">
|
||||||
<optgroup label="{t}Generic formats{/t}">
|
<optgroup label="{t}Generic formats{/t}">
|
||||||
<option value="best{$protocol}">
|
<option value="{$defaultFormat}">
|
||||||
{strip}
|
{strip}
|
||||||
{t}Best{/t} ({$video->ext})
|
{t}Best{/t} ({$video->ext})
|
||||||
{/strip}
|
{/strip}
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
{t}Remux best video with best audio{/t}
|
{t}Remux best video with best audio{/t}
|
||||||
</option>
|
</option>
|
||||||
{/if}
|
{/if}
|
||||||
<option value="worst{$protocol}">
|
<option value="{$defaultFormat|replace:best:worst}">
|
||||||
{t}Worst{/t}
|
{t}Worst{/t}
|
||||||
</option>
|
</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
|
@ -80,17 +80,23 @@
|
||||||
{/foreach}
|
{/foreach}
|
||||||
</optgroup>
|
</optgroup>
|
||||||
</select><br/><br/>
|
</select><br/><br/>
|
||||||
|
{if $config->stream}
|
||||||
|
<input type="checkbox" name="stream" id="stream"/>
|
||||||
|
<label for="stream">{t}Stream the video through the server{/t}</label>
|
||||||
|
<br/><br/>
|
||||||
|
{/if}
|
||||||
{if $config->convertAdvanced}
|
{if $config->convertAdvanced}
|
||||||
<input type="checkbox" name="customConvert" id="customConvert"/>
|
<input type="checkbox" name="customConvert" id="customConvert"/>
|
||||||
<label for="customConvert">{t}Convert into a custom format:{/t}</label>
|
<label for="customConvert">{t}Convert into a custom format:{/t}</label>
|
||||||
<select title="Custom format" name="customFormat">
|
<select title="Custom format" name="customFormat" aria-label="{t}Format to convert to{/t}">
|
||||||
{foreach $config->convertAdvancedFormats as $format}
|
{foreach $config->convertAdvancedFormats as $format}
|
||||||
<option>{$format}</option>
|
<option>{$format}</option>
|
||||||
{/foreach}
|
{/foreach}
|
||||||
</select>
|
</select>
|
||||||
{t}with{/t}
|
{t}with{/t}
|
||||||
<input type="number" value="{$config->audioBitrate}" title="Custom bitrate" class="customBitrate"name="customBitrate" id="customBitrate" />
|
<label for="customBitrate" class="sr-only">{t}Bit rate{/t}</label>
|
||||||
<label for="customBitrate">{t}kbit/s audio{/t}</label>
|
<input type="number" value="{$config->audioBitrate}" title="Custom bitrate" class="customBitrate"name="customBitrate" id="customBitrate" aria-describedby="customBitrateUnit" />
|
||||||
|
<span id="customBitrateUnit">{t}kbit/s audio{/t}</span>
|
||||||
<br/><br/>
|
<br/><br/>
|
||||||
{/if}
|
{/if}
|
||||||
<input class="downloadBtn" type="submit" value="{t}Download{/t}" /><br/>
|
<input class="downloadBtn" type="submit" value="{t}Download{/t}" /><br/>
|
|
@ -5,7 +5,8 @@
|
||||||
<h2>{t}This video is protected{/t}</h2>
|
<h2>{t}This video is protected{/t}</h2>
|
||||||
<p>{t}You need a password in order to download this video.{/t}</p>
|
<p>{t}You need a password in order to download this video.{/t}</p>
|
||||||
<form action="" method="POST">
|
<form action="" method="POST">
|
||||||
<input class="URLinput" type="password" name="password" title="{t}Video password{/t}" />
|
<label class="sr-only" for="password">{t}Video password{/t}</label>
|
||||||
|
<input class="URLinput" type="password" name="password" id="password" />
|
||||||
<br/><br/>
|
<br/><br/>
|
||||||
<input class="downloadBtn" type="submit" value="{t}Download{/t}" />
|
<input class="downloadBtn" type="submit" value="{t}Download{/t}" />
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -7,24 +7,28 @@
|
||||||
{$video->title}</a></i>{/if}{t}:{/t}
|
{$video->title}</a></i>{/if}{t}:{/t}
|
||||||
</p>
|
</p>
|
||||||
{if $config->stream}
|
{if $config->stream}
|
||||||
<a href="{path_for name="redirect"}?url={$video->webpage_url}" class="downloadBtn">Download everything</a>
|
<a href="{path_for name="download"}?url={$video->webpage_url}" class="downloadBtn">Download everything</a>
|
||||||
{/if}
|
{/if}
|
||||||
{foreach $video->entries as $video}
|
{foreach $video->entries as $entry}
|
||||||
<div class="playlist-entry">
|
<div class="playlist-entry">
|
||||||
<h3 class="playlist-entry-title"><a target="_blank" href="{strip}
|
<h3 class="playlist-entry-title"><a target="_blank" href="{strip}
|
||||||
{if isset($video->ie_key) and $video->ie_key == Youtube and !filter_var($video->url, FILTER_VALIDATE_URL)}
|
{if isset($entry->ie_key) and $entry->ie_key == Youtube and !filter_var($entry->url, FILTER_VALIDATE_URL)}
|
||||||
https://www.youtube.com/watch?v=
|
https://www.youtube.com/watch?v=
|
||||||
{/if}
|
{/if}
|
||||||
{$video->url}
|
{$entry->url}
|
||||||
{/strip}">
|
{/strip}">
|
||||||
{if !isset($video->title) and $video->ie_key == YoutubePlaylist}
|
{if !isset($entry->title)}
|
||||||
|
{if $entry->ie_key == YoutubePlaylist}
|
||||||
Playlist
|
Playlist
|
||||||
{else}
|
{else}
|
||||||
{$video->title}
|
Video
|
||||||
|
{/if}
|
||||||
|
{else}
|
||||||
|
{$entry->title}
|
||||||
{/if}
|
{/if}
|
||||||
</a></h3>
|
</a></h3>
|
||||||
<a target="_blank" class="downloadBtn" href="{path_for name="redirect"}?url={$video->url}">{t}Download{/t}</a>
|
<a target="_blank" class="downloadBtn" href="{path_for name="download"}?url={$entry->url}">{t}Download{/t}</a>
|
||||||
<a target="_blank" href="{path_for name="video"}?url={$video->url}">{t}More options{/t}</a>
|
<a target="_blank" href="{path_for name="info"}?url={$entry->url}">{t}More options{/t}</a>
|
||||||
</div>
|
</div>
|
||||||
{/foreach}
|
{/foreach}
|
||||||
|
|
||||||
|
|
47
tests/BaseTest.php
Normal file
47
tests/BaseTest.php
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* PlaylistArchiveStreamTest class.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Alltube\Test;
|
||||||
|
|
||||||
|
use Alltube\Config;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class used by every test.
|
||||||
|
*/
|
||||||
|
abstract class BaseTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the config file used in tests.
|
||||||
|
*
|
||||||
|
* @return string Path to file
|
||||||
|
*/
|
||||||
|
protected function getConfigFile()
|
||||||
|
{
|
||||||
|
if (PHP_OS == 'WINNT') {
|
||||||
|
$configFile = 'config_test_windows.yml';
|
||||||
|
} else {
|
||||||
|
$configFile = 'config_test.yml';
|
||||||
|
}
|
||||||
|
|
||||||
|
return __DIR__.'/../config/'.$configFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare tests.
|
||||||
|
*/
|
||||||
|
protected function setUp()
|
||||||
|
{
|
||||||
|
Config::setFile($this->getConfigFile());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy properties after test.
|
||||||
|
*/
|
||||||
|
protected function tearDown()
|
||||||
|
{
|
||||||
|
Config::destroyInstance();
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,12 +6,11 @@
|
||||||
namespace Alltube\Test;
|
namespace Alltube\Test;
|
||||||
|
|
||||||
use Alltube\Config;
|
use Alltube\Config;
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit tests for the Config class.
|
* Unit tests for the Config class.
|
||||||
*/
|
*/
|
||||||
class ConfigTest extends TestCase
|
class ConfigTest extends BaseTest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Config class instance.
|
* Config class instance.
|
||||||
|
@ -25,17 +24,9 @@ class ConfigTest extends TestCase
|
||||||
*/
|
*/
|
||||||
protected function setUp()
|
protected function setUp()
|
||||||
{
|
{
|
||||||
$this->config = Config::getInstance('config/config_test.yml');
|
parent::setUp();
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
$this->config = Config::getInstance();
|
||||||
* Destroy variables created by setUp().
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function tearDown()
|
|
||||||
{
|
|
||||||
Config::destroyInstance();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -45,8 +36,9 @@ class ConfigTest extends TestCase
|
||||||
*/
|
*/
|
||||||
public function testGetInstance()
|
public function testGetInstance()
|
||||||
{
|
{
|
||||||
$this->assertEquals($this->config->convert, false);
|
$config = Config::getInstance();
|
||||||
$this->assertConfig($this->config);
|
$this->assertEquals($config->convert, false);
|
||||||
|
$this->assertConfig($config);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -70,25 +62,77 @@ class ConfigTest extends TestCase
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the getInstance function with a missing config file.
|
* Test the setFile function.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testSetFile()
|
||||||
|
{
|
||||||
|
Config::setFile($this->getConfigFile());
|
||||||
|
$this->assertConfig($this->config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the setFile function with a missing config file.
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
* @expectedException Exception
|
* @expectedException Exception
|
||||||
*/
|
*/
|
||||||
public function testGetInstanceWithMissingFile()
|
public function testSetFileWithMissingFile()
|
||||||
{
|
{
|
||||||
Config::getInstance('foo');
|
Config::setFile('foo');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the getInstance function with an empty filename.
|
* Test the setOptions function.
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function testGetInstanceWithEmptyFile()
|
public function testSetOptions()
|
||||||
{
|
{
|
||||||
$config = Config::getInstance('');
|
Config::setOptions(['appName' => 'foo']);
|
||||||
$this->assertConfig($config);
|
$config = Config::getInstance();
|
||||||
|
$this->assertEquals($config->appName, 'foo');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the setOptions function.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testSetOptionsWithoutUpdate()
|
||||||
|
{
|
||||||
|
if (getenv('APPVEYOR')) {
|
||||||
|
$this->markTestSkipped(
|
||||||
|
"This will fail on AppVeyor because it won't be able to find youtube-dl at the defaut path."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Config::setOptions(['appName' => 'foo'], false);
|
||||||
|
$config = Config::getInstance();
|
||||||
|
$this->assertEquals($config->appName, 'foo');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the setOptions function.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
* @expectedException Exception
|
||||||
|
*/
|
||||||
|
public function testSetOptionsWithBadYoutubedl()
|
||||||
|
{
|
||||||
|
Config::setOptions(['youtubedl' => 'foo']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the setOptions function.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
* @expectedException Exception
|
||||||
|
*/
|
||||||
|
public function testSetOptionsWithBadPython()
|
||||||
|
{
|
||||||
|
Config::setOptions(['python' => 'foo']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -100,11 +144,9 @@ class ConfigTest extends TestCase
|
||||||
{
|
{
|
||||||
Config::destroyInstance();
|
Config::destroyInstance();
|
||||||
putenv('CONVERT=1');
|
putenv('CONVERT=1');
|
||||||
putenv('PYTHON=foo');
|
Config::setFile($this->getConfigFile());
|
||||||
$config = Config::getInstance('config/config_test.yml');
|
$config = Config::getInstance();
|
||||||
$this->assertEquals($config->convert, true);
|
$this->assertEquals($config->convert, true);
|
||||||
$this->assertEquals($config->python, 'foo');
|
|
||||||
putenv('CONVERT');
|
putenv('CONVERT');
|
||||||
putenv('PYTHON');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
143
tests/ControllerTest.php
Normal file
143
tests/ControllerTest.php
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* ControllerTest class.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Alltube\Test;
|
||||||
|
|
||||||
|
use Alltube\Controller\DownloadController;
|
||||||
|
use Alltube\Controller\FrontController;
|
||||||
|
use Alltube\LocaleManager;
|
||||||
|
use Alltube\ViewFactory;
|
||||||
|
use Slim\Container;
|
||||||
|
use Slim\Http\Environment;
|
||||||
|
use Slim\Http\Request;
|
||||||
|
use Slim\Http\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class used by the controller tests.
|
||||||
|
*/
|
||||||
|
abstract class ControllerTest extends BaseTest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Slim dependency container.
|
||||||
|
*
|
||||||
|
* @var Container
|
||||||
|
*/
|
||||||
|
protected $container;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock HTTP request.
|
||||||
|
*
|
||||||
|
* @var Request
|
||||||
|
*/
|
||||||
|
protected $request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock HTTP response.
|
||||||
|
*
|
||||||
|
* @var Response
|
||||||
|
*/
|
||||||
|
protected $response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller instance used in tests.
|
||||||
|
*/
|
||||||
|
protected $controller;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare tests.
|
||||||
|
*/
|
||||||
|
protected function setUp()
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->container = new Container();
|
||||||
|
$this->request = Request::createFromEnvironment(Environment::mock());
|
||||||
|
$this->response = new Response();
|
||||||
|
$this->container['view'] = ViewFactory::create($this->container, $this->request);
|
||||||
|
$this->container['locale'] = new LocaleManager();
|
||||||
|
|
||||||
|
$frontController = new FrontController($this->container);
|
||||||
|
$downloadController = new DownloadController($this->container);
|
||||||
|
|
||||||
|
$this->container['router']->map(['GET'], '/', [$frontController, 'index'])
|
||||||
|
->setName('index');
|
||||||
|
$this->container['router']->map(['GET'], '/video', [$frontController, 'info'])
|
||||||
|
->setName('info');
|
||||||
|
$this->container['router']->map(['GET'], '/extractors', [$frontController, 'extractors'])
|
||||||
|
->setName('extractors');
|
||||||
|
$this->container['router']->map(['GET'], '/locale', [$frontController, 'locale'])
|
||||||
|
->setName('locale');
|
||||||
|
$this->container['router']->map(['GET'], '/redirect', [$downloadController, 'download'])
|
||||||
|
->setName('download');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run controller function with custom query parameters and return the result.
|
||||||
|
*
|
||||||
|
* @param string $request Controller function to call
|
||||||
|
* @param array $params Query parameters
|
||||||
|
*
|
||||||
|
* @return Response HTTP response
|
||||||
|
*/
|
||||||
|
protected function getRequestResult($request, array $params)
|
||||||
|
{
|
||||||
|
return $this->controller->$request(
|
||||||
|
$this->request->withQueryParams($params),
|
||||||
|
$this->response
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that calling controller function with these parameters returns a 200 HTTP response.
|
||||||
|
*
|
||||||
|
* @param string $request Controller function to call
|
||||||
|
* @param array $params Query parameters
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function assertRequestIsOk($request, array $params = [])
|
||||||
|
{
|
||||||
|
$this->assertTrue($this->getRequestResult($request, $params)->isOk());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that calling controller function with these parameters returns an HTTP redirect.
|
||||||
|
*
|
||||||
|
* @param string $request Controller function to call
|
||||||
|
* @param array $params Query parameters
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function assertRequestIsRedirect($request, array $params = [])
|
||||||
|
{
|
||||||
|
$this->assertTrue($this->getRequestResult($request, $params)->isRedirect());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that calling controller function with these parameters returns an HTTP 500 error.
|
||||||
|
*
|
||||||
|
* @param string $request Controller function to call
|
||||||
|
* @param array $params Query parameters
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function assertRequestIsServerError($request, array $params = [])
|
||||||
|
{
|
||||||
|
$this->assertTrue($this->getRequestResult($request, $params)->isServerError());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that calling controller function with these parameters returns an HTTP 400 error.
|
||||||
|
*
|
||||||
|
* @param string $request Controller function to call
|
||||||
|
* @param array $params Query parameters
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function assertRequestIsClientError($request, array $params = [])
|
||||||
|
{
|
||||||
|
$this->assertTrue($this->getRequestResult($request, $params)->isClientError());
|
||||||
|
}
|
||||||
|
}
|
27
tests/ConvertedPlaylistArchiveStreamTest.php
Normal file
27
tests/ConvertedPlaylistArchiveStreamTest.php
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* ConvertedPlaylistArchiveStreamTest class.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Alltube\Test;
|
||||||
|
|
||||||
|
use Alltube\Stream\ConvertedPlaylistArchiveStream;
|
||||||
|
use Alltube\Video;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for the ConvertedPlaylistArchiveStream class.
|
||||||
|
*/
|
||||||
|
class ConvertedPlaylistArchiveStreamTest extends StreamTest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Prepare tests.
|
||||||
|
*/
|
||||||
|
protected function setUp()
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$video = new Video('https://www.youtube.com/playlist?list=PL1j4Ff8cAqPu5iowaeUAY8lRgkfT4RybJ');
|
||||||
|
|
||||||
|
$this->stream = new ConvertedPlaylistArchiveStream($video);
|
||||||
|
}
|
||||||
|
}
|
221
tests/DownloadControllerTest.php
Normal file
221
tests/DownloadControllerTest.php
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* DownloadControllerTest class.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Alltube\Test;
|
||||||
|
|
||||||
|
use Alltube\Config;
|
||||||
|
use Alltube\Controller\DownloadController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for the FrontController class.
|
||||||
|
*/
|
||||||
|
class DownloadControllerTest extends ControllerTest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Prepare tests.
|
||||||
|
*/
|
||||||
|
protected function setUp()
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->controller = new DownloadController($this->container);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the download() function without the URL parameter.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testDownloadWithoutUrl()
|
||||||
|
{
|
||||||
|
$this->assertRequestIsRedirect('download');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the download() function.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testDownload()
|
||||||
|
{
|
||||||
|
$this->assertRequestIsRedirect('download', ['url' => 'https://www.youtube.com/watch?v=M7IpKCZ47pU']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the download() function with a specific format.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testDownloadWithFormat()
|
||||||
|
{
|
||||||
|
$this->assertRequestIsRedirect(
|
||||||
|
'download',
|
||||||
|
['url' => 'https://www.youtube.com/watch?v=M7IpKCZ47pU', 'format' => 'worst']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the download() function with streams enabled.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testDownloadWithStream()
|
||||||
|
{
|
||||||
|
Config::setOptions(['stream' => true]);
|
||||||
|
|
||||||
|
$this->assertRequestIsOk(
|
||||||
|
'download',
|
||||||
|
['url' => 'https://www.youtube.com/watch?v=M7IpKCZ47pU', 'stream' => true]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the download() function with an M3U stream.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testDownloadWithM3uStream()
|
||||||
|
{
|
||||||
|
if (getenv('CI')) {
|
||||||
|
$this->markTestSkipped('Twitter returns a 429 error when the test is ran too many times.');
|
||||||
|
}
|
||||||
|
|
||||||
|
Config::setOptions(['stream' => true]);
|
||||||
|
|
||||||
|
$this->assertRequestIsOk(
|
||||||
|
'download',
|
||||||
|
[
|
||||||
|
'url' => 'https://twitter.com/verge/status/813055465324056576/video/1',
|
||||||
|
'format' => 'hls-2176',
|
||||||
|
'stream' => true,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the download() function with an RTMP stream.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testDownloadWithRtmpStream()
|
||||||
|
{
|
||||||
|
$this->markTestIncomplete('We need to find another RTMP video.');
|
||||||
|
|
||||||
|
Config::setOptions(['stream' => true]);
|
||||||
|
|
||||||
|
$this->assertRequestIsOk(
|
||||||
|
'download',
|
||||||
|
['url' => 'http://www.rtvnh.nl/video/131946', 'format' => 'rtmp-264']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the download() function with a remuxed video.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testDownloadWithRemux()
|
||||||
|
{
|
||||||
|
Config::setOptions(['remux' => true]);
|
||||||
|
|
||||||
|
$this->assertRequestIsOk(
|
||||||
|
'download',
|
||||||
|
[
|
||||||
|
'url' => 'https://www.youtube.com/watch?v=M7IpKCZ47pU',
|
||||||
|
'format' => 'bestvideo+bestaudio',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the download() function with a remuxed video but remux disabled.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testDownloadWithRemuxDisabled()
|
||||||
|
{
|
||||||
|
$this->assertRequestIsServerError(
|
||||||
|
'download',
|
||||||
|
[
|
||||||
|
'url' => 'https://www.youtube.com/watch?v=M7IpKCZ47pU',
|
||||||
|
'format' => 'bestvideo+bestaudio',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the download() function with a missing password.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testDownloadWithMissingPassword()
|
||||||
|
{
|
||||||
|
if (getenv('CI')) {
|
||||||
|
$this->markTestSkipped('Travis is blacklisted by Vimeo.');
|
||||||
|
}
|
||||||
|
$this->assertRequestIsRedirect('download', ['url' => 'http://vimeo.com/68375962']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the download() function with an error.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testDownloadWithError()
|
||||||
|
{
|
||||||
|
$this->assertRequestIsServerError('download', ['url' => 'http://example.com/foo']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the download() function with an video that returns an empty URL.
|
||||||
|
* This can be caused by trying to redirect to a playlist.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testDownloadWithEmptyUrl()
|
||||||
|
{
|
||||||
|
$this->assertRequestIsServerError(
|
||||||
|
'download',
|
||||||
|
['url' => 'https://www.youtube.com/playlist?list=PLgdySZU6KUXL_8Jq5aUkyNV7wCa-4wZsC']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the download() function with a playlist stream.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
* @requires OS Linux
|
||||||
|
*/
|
||||||
|
public function testDownloadWithPlaylist()
|
||||||
|
{
|
||||||
|
Config::setOptions(['stream' => true]);
|
||||||
|
|
||||||
|
$this->assertRequestIsOk(
|
||||||
|
'download',
|
||||||
|
['url' => 'https://www.youtube.com/playlist?list=PLgdySZU6KUXL_8Jq5aUkyNV7wCa-4wZsC']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the download() function with an advanced conversion.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testDownloadWithAdvancedConversion()
|
||||||
|
{
|
||||||
|
Config::setOptions(['convertAdvanced' => true]);
|
||||||
|
|
||||||
|
$this->assertRequestIsOk(
|
||||||
|
'download',
|
||||||
|
[
|
||||||
|
'url' => 'https://www.youtube.com/watch?v=M7IpKCZ47pU',
|
||||||
|
'format' => 'best',
|
||||||
|
'customConvert' => 'on',
|
||||||
|
'customBitrate' => 32,
|
||||||
|
'customFormat' => 'flv',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,171 +7,23 @@ namespace Alltube\Test;
|
||||||
|
|
||||||
use Alltube\Config;
|
use Alltube\Config;
|
||||||
use Alltube\Controller\FrontController;
|
use Alltube\Controller\FrontController;
|
||||||
use Alltube\LocaleManager;
|
|
||||||
use Alltube\ViewFactory;
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use Slim\Container;
|
|
||||||
use Slim\Http\Environment;
|
use Slim\Http\Environment;
|
||||||
use Slim\Http\Request;
|
use Slim\Http\Request;
|
||||||
use Slim\Http\Response;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit tests for the FrontController class.
|
* Unit tests for the FrontController class.
|
||||||
*/
|
*/
|
||||||
class FrontControllerTest extends TestCase
|
class FrontControllerTest extends ControllerTest
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Slim dependency container.
|
|
||||||
*
|
|
||||||
* @var Container
|
|
||||||
*/
|
|
||||||
private $container;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mock HTTP request.
|
|
||||||
*
|
|
||||||
* @var Request
|
|
||||||
*/
|
|
||||||
private $request;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mock HTTP response.
|
|
||||||
*
|
|
||||||
* @var Response
|
|
||||||
*/
|
|
||||||
private $response;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* FrontController instance used in tests.
|
|
||||||
*
|
|
||||||
* @var FrontController
|
|
||||||
*/
|
|
||||||
private $controller;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Config class instance.
|
|
||||||
*
|
|
||||||
* @var Config
|
|
||||||
*/
|
|
||||||
private $config;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepare tests.
|
* Prepare tests.
|
||||||
*/
|
*/
|
||||||
protected function setUp()
|
protected function setUp()
|
||||||
{
|
{
|
||||||
$this->container = new Container();
|
parent::setUp();
|
||||||
$this->request = Request::createFromEnvironment(Environment::mock());
|
|
||||||
$this->response = new Response();
|
|
||||||
$this->container['view'] = ViewFactory::create($this->container, $this->request);
|
|
||||||
$this->container['locale'] = new LocaleManager();
|
|
||||||
|
|
||||||
if (PHP_OS == 'WINNT') {
|
$this->controller = new FrontController($this->container);
|
||||||
$configFile = 'config_test_windows.yml';
|
|
||||||
} else {
|
|
||||||
$configFile = 'config_test.yml';
|
|
||||||
}
|
|
||||||
$this->config = Config::getInstance('config/'.$configFile);
|
|
||||||
$this->controller = new FrontController($this->container, $this->config);
|
|
||||||
|
|
||||||
$this->container['router']->map(['GET'], '/', [$this->controller, 'index'])
|
|
||||||
->setName('index');
|
|
||||||
$this->container['router']->map(['GET'], '/video', [$this->controller, 'video'])
|
|
||||||
->setName('video');
|
|
||||||
$this->container['router']->map(['GET'], '/extractors', [$this->controller, 'extractors'])
|
|
||||||
->setName('extractors');
|
|
||||||
$this->container['router']->map(['GET'], '/redirect', [$this->controller, 'redirect'])
|
|
||||||
->setName('redirect');
|
|
||||||
$this->container['router']->map(['GET'], '/locale', [$this->controller, 'locale'])
|
|
||||||
->setName('locale');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Destroy properties after test.
|
|
||||||
*/
|
|
||||||
protected function tearDown()
|
|
||||||
{
|
|
||||||
Config::destroyInstance();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run controller function with custom query parameters and return the result.
|
|
||||||
*
|
|
||||||
* @param string $request Controller function to call
|
|
||||||
* @param array $params Query parameters
|
|
||||||
* @param Config $config Custom config
|
|
||||||
*
|
|
||||||
* @return Response HTTP response
|
|
||||||
*/
|
|
||||||
private function getRequestResult($request, array $params, Config $config = null)
|
|
||||||
{
|
|
||||||
if (isset($config)) {
|
|
||||||
$controller = new FrontController($this->container, $config);
|
|
||||||
} else {
|
|
||||||
$controller = $this->controller;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $controller->$request(
|
|
||||||
$this->request->withQueryParams($params),
|
|
||||||
$this->response
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assert that calling controller function with these parameters returns a 200 HTTP response.
|
|
||||||
*
|
|
||||||
* @param string $request Controller function to call
|
|
||||||
* @param array $params Query parameters
|
|
||||||
* @param Config $config Custom config
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
private function assertRequestIsOk($request, array $params = [], Config $config = null)
|
|
||||||
{
|
|
||||||
$this->assertTrue($this->getRequestResult($request, $params, $config)->isOk());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assert that calling controller function with these parameters returns an HTTP redirect.
|
|
||||||
*
|
|
||||||
* @param string $request Controller function to call
|
|
||||||
* @param array $params Query parameters
|
|
||||||
* @param Config $config Custom config
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
private function assertRequestIsRedirect($request, array $params = [], Config $config = null)
|
|
||||||
{
|
|
||||||
$this->assertTrue($this->getRequestResult($request, $params, $config)->isRedirect());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assert that calling controller function with these parameters returns an HTTP 500 error.
|
|
||||||
*
|
|
||||||
* @param string $request Controller function to call
|
|
||||||
* @param array $params Query parameters
|
|
||||||
* @param Config $config Custom config
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
private function assertRequestIsServerError($request, array $params = [], Config $config = null)
|
|
||||||
{
|
|
||||||
$this->assertTrue($this->getRequestResult($request, $params, $config)->isServerError());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assert that calling controller function with these parameters returns an HTTP 400 error.
|
|
||||||
*
|
|
||||||
* @param string $request Controller function to call
|
|
||||||
* @param array $params Query parameters
|
|
||||||
* @param Config $config Custom config
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
private function assertRequestIsClientError($request, array $params = [], Config $config = null)
|
|
||||||
{
|
|
||||||
$this->assertTrue($this->getRequestResult($request, $params, $config)->isClientError());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -181,20 +33,7 @@ class FrontControllerTest extends TestCase
|
||||||
*/
|
*/
|
||||||
public function testConstructor()
|
public function testConstructor()
|
||||||
{
|
{
|
||||||
$controller = new FrontController($this->container, $this->config);
|
$this->assertInstanceOf(FrontController::class, new FrontController($this->container));
|
||||||
$this->assertInstanceOf(FrontController::class, $controller);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test the constructor with a default config.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
* @requires OS Linux
|
|
||||||
*/
|
|
||||||
public function testConstructorWithDefaultConfig()
|
|
||||||
{
|
|
||||||
$controller = new FrontController($this->container);
|
|
||||||
$this->assertInstanceOf(FrontController::class, $controller);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -204,9 +43,8 @@ class FrontControllerTest extends TestCase
|
||||||
*/
|
*/
|
||||||
public function testConstructorWithStream()
|
public function testConstructorWithStream()
|
||||||
{
|
{
|
||||||
$this->config->stream = true;
|
Config::setOptions(['stream' => true]);
|
||||||
$controller = new FrontController($this->container, $this->config);
|
$this->assertInstanceOf(FrontController::class, new FrontController($this->container));
|
||||||
$this->assertInstanceOf(FrontController::class, $controller);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -256,55 +94,67 @@ class FrontControllerTest extends TestCase
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the video() function without the url parameter.
|
* Test the info() function without the url parameter.
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function testVideoWithoutUrl()
|
public function testInfoWithoutUrl()
|
||||||
{
|
{
|
||||||
$this->assertRequestIsRedirect('video');
|
$this->assertRequestIsRedirect('info');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the video() function.
|
* Test the info() function.
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function testVideo()
|
public function testInfo()
|
||||||
{
|
{
|
||||||
$this->assertRequestIsOk('video', ['url' => 'https://www.youtube.com/watch?v=M7IpKCZ47pU']);
|
$this->assertRequestIsOk('info', ['url' => 'https://www.youtube.com/watch?v=M7IpKCZ47pU']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the video() function with audio conversion.
|
* Test the info() function with audio conversion.
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function testVideoWithAudio()
|
public function testInfoWithAudio()
|
||||||
{
|
{
|
||||||
$this->assertRequestIsOk('video', ['url' => 'https://www.youtube.com/watch?v=M7IpKCZ47pU', 'audio' => true]);
|
Config::setOptions(['convert' => true]);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test the video() function with audio conversion from a Vimeo video.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testVideoWithVimeoAudio()
|
|
||||||
{
|
|
||||||
// So we can test the fallback to default format
|
|
||||||
$this->assertRequestIsOk('video', ['url' => 'https://vimeo.com/251997032', 'audio' => true]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test the video() function with audio enabled and an URL that doesn't need to be converted.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testVideoWithUnconvertedAudio()
|
|
||||||
{
|
|
||||||
$this->assertRequestIsRedirect(
|
$this->assertRequestIsRedirect(
|
||||||
'video',
|
'info',
|
||||||
|
['url' => 'https://www.youtube.com/watch?v=M7IpKCZ47pU', 'audio' => true]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the info() function with audio conversion from a Vimeo video.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testInfoWithVimeoAudio()
|
||||||
|
{
|
||||||
|
if (getenv('CI')) {
|
||||||
|
$this->markTestSkipped('Travis is blacklisted by Vimeo.');
|
||||||
|
}
|
||||||
|
Config::setOptions(['convert' => true]);
|
||||||
|
|
||||||
|
// So we can test the fallback to default format
|
||||||
|
$this->assertRequestIsRedirect('info', ['url' => 'https://vimeo.com/251997032', 'audio' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the info() function with audio enabled and an URL that doesn't need to be converted.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testInfoWithUnconvertedAudio()
|
||||||
|
{
|
||||||
|
Config::setOptions(['convert' => true]);
|
||||||
|
|
||||||
|
$this->assertRequestIsRedirect(
|
||||||
|
'info',
|
||||||
[
|
[
|
||||||
'url' => 'https://2080.bandcamp.com/track/cygnus-x-the-orange-theme-2080-faulty-chip-cover',
|
'url' => 'https://2080.bandcamp.com/track/cygnus-x-the-orange-theme-2080-faulty-chip-cover',
|
||||||
'audio' => true,
|
'audio' => true,
|
||||||
|
@ -313,13 +163,16 @@ class FrontControllerTest extends TestCase
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the video() function with a password.
|
* Test the info() function with a password.
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function testVideoWithPassword()
|
public function testInfoWithPassword()
|
||||||
{
|
{
|
||||||
$result = $this->controller->video(
|
if (getenv('CI')) {
|
||||||
|
$this->markTestSkipped('Travis is blacklisted by Vimeo.');
|
||||||
|
}
|
||||||
|
$result = $this->controller->info(
|
||||||
$this->request->withQueryParams(['url' => 'http://vimeo.com/68375962'])
|
$this->request->withQueryParams(['url' => 'http://vimeo.com/68375962'])
|
||||||
->withParsedBody(['password' => 'youtube-dl']),
|
->withParsedBody(['password' => 'youtube-dl']),
|
||||||
$this->response
|
$this->response
|
||||||
|
@ -328,41 +181,44 @@ class FrontControllerTest extends TestCase
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the video() function with a missing password.
|
* Test the info() function with a missing password.
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function testVideoWithMissingPassword()
|
public function testInfoWithMissingPassword()
|
||||||
{
|
{
|
||||||
$this->assertRequestIsOk('video', ['url' => 'http://vimeo.com/68375962']);
|
if (getenv('CI')) {
|
||||||
$this->assertRequestIsOk('video', ['url' => 'http://vimeo.com/68375962', 'audio' => true]);
|
$this->markTestSkipped('Travis is blacklisted by Vimeo.');
|
||||||
|
}
|
||||||
|
$this->assertRequestIsOk('info', ['url' => 'http://vimeo.com/68375962']);
|
||||||
|
$this->assertRequestIsOk('info', ['url' => 'http://vimeo.com/68375962', 'audio' => true]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the video() function with streams enabled.
|
* Test the info() function with streams enabled.
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function testVideoWithStream()
|
public function testInfoWithStream()
|
||||||
{
|
{
|
||||||
$this->config->stream = true;
|
Config::setOptions(['stream' => true]);
|
||||||
$this->assertRequestIsOk('video', ['url' => 'https://www.youtube.com/watch?v=M7IpKCZ47pU'], $this->config);
|
|
||||||
|
$this->assertRequestIsOk('info', ['url' => 'https://www.youtube.com/watch?v=M7IpKCZ47pU']);
|
||||||
$this->assertRequestIsOk(
|
$this->assertRequestIsOk(
|
||||||
'video',
|
'info',
|
||||||
['url' => 'https://www.youtube.com/watch?v=M7IpKCZ47pU', 'audio' => true],
|
['url' => 'https://www.youtube.com/watch?v=M7IpKCZ47pU', 'audio' => true]
|
||||||
$this->config
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the video() function with a playlist.
|
* Test the info() function with a playlist.
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function testVideoWithPlaylist()
|
public function testInfoWithPlaylist()
|
||||||
{
|
{
|
||||||
$this->assertRequestIsOk(
|
$this->assertRequestIsOk(
|
||||||
'video',
|
'info',
|
||||||
['url' => 'https://www.youtube.com/playlist?list=PLgdySZU6KUXL_8Jq5aUkyNV7wCa-4wZsC']
|
['url' => 'https://www.youtube.com/playlist?list=PLgdySZU6KUXL_8Jq5aUkyNV7wCa-4wZsC']
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -378,224 +234,6 @@ class FrontControllerTest extends TestCase
|
||||||
$this->assertTrue($result->isServerError());
|
$this->assertTrue($result->isServerError());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Test the redirect() function without the URL parameter.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testRedirectWithoutUrl()
|
|
||||||
{
|
|
||||||
$this->assertRequestIsRedirect('redirect');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test the redirect() function.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testRedirect()
|
|
||||||
{
|
|
||||||
$this->assertRequestIsRedirect('redirect', ['url' => 'https://www.youtube.com/watch?v=M7IpKCZ47pU']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test the redirect() function with a specific format.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testRedirectWithFormat()
|
|
||||||
{
|
|
||||||
$this->assertRequestIsRedirect(
|
|
||||||
'redirect',
|
|
||||||
['url' => 'https://www.youtube.com/watch?v=M7IpKCZ47pU', 'format' => 'worst']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test the redirect() function with streams enabled.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testRedirectWithStream()
|
|
||||||
{
|
|
||||||
$this->config->stream = true;
|
|
||||||
$this->assertRequestIsOk(
|
|
||||||
'redirect',
|
|
||||||
['url' => 'https://www.youtube.com/watch?v=M7IpKCZ47pU'],
|
|
||||||
$this->config
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test the redirect() function with an M3U stream.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testRedirectWithM3uStream()
|
|
||||||
{
|
|
||||||
$this->config->stream = true;
|
|
||||||
$this->assertRequestIsOk(
|
|
||||||
'redirect',
|
|
||||||
[
|
|
||||||
'url' => 'https://twitter.com/verge/status/813055465324056576/video/1',
|
|
||||||
'format' => 'hls-2176',
|
|
||||||
],
|
|
||||||
$this->config
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test the redirect() function with an RTMP stream.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testRedirectWithRtmpStream()
|
|
||||||
{
|
|
||||||
$this->markTestIncomplete('We need to find another RTMP video.');
|
|
||||||
|
|
||||||
$this->config->stream = true;
|
|
||||||
$this->assertRequestIsOk(
|
|
||||||
'redirect',
|
|
||||||
['url' => 'http://www.rtvnh.nl/video/131946', 'format' => 'rtmp-264'],
|
|
||||||
$this->config
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test the redirect() function with a remuxed video.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testRedirectWithRemux()
|
|
||||||
{
|
|
||||||
$this->config->remux = true;
|
|
||||||
$this->assertRequestIsOk(
|
|
||||||
'redirect',
|
|
||||||
[
|
|
||||||
'url' => 'https://www.youtube.com/watch?v=M7IpKCZ47pU',
|
|
||||||
'format' => 'bestvideo+bestaudio',
|
|
||||||
],
|
|
||||||
$this->config
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test the redirect() function with a remuxed video but remux disabled.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testRedirectWithRemuxDisabled()
|
|
||||||
{
|
|
||||||
$this->assertRequestIsServerError(
|
|
||||||
'redirect',
|
|
||||||
[
|
|
||||||
'url' => 'https://www.youtube.com/watch?v=M7IpKCZ47pU',
|
|
||||||
'format' => 'bestvideo+bestaudio',
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test the redirect() function with a missing password.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testRedirectWithMissingPassword()
|
|
||||||
{
|
|
||||||
$this->assertRequestIsRedirect('redirect', ['url' => 'http://vimeo.com/68375962']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test the redirect() function with an error.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testRedirectWithError()
|
|
||||||
{
|
|
||||||
$this->assertRequestIsServerError('redirect', ['url' => 'http://example.com/foo']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test the redirect() function with an video that returns an empty URL.
|
|
||||||
* This can be caused by trying to redirect to a playlist.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testRedirectWithEmptyUrl()
|
|
||||||
{
|
|
||||||
$this->assertRequestIsServerError(
|
|
||||||
'redirect',
|
|
||||||
['url' => 'https://www.youtube.com/playlist?list=PLgdySZU6KUXL_8Jq5aUkyNV7wCa-4wZsC']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test the redirect() function with a playlist stream.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
* @requires OS Linux
|
|
||||||
*/
|
|
||||||
public function testRedirectWithPlaylist()
|
|
||||||
{
|
|
||||||
$this->config->stream = true;
|
|
||||||
$this->assertRequestIsOk(
|
|
||||||
'redirect',
|
|
||||||
['url' => 'https://www.youtube.com/playlist?list=PLgdySZU6KUXL_8Jq5aUkyNV7wCa-4wZsC'],
|
|
||||||
$this->config
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test the redirect() function with an advanced conversion.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testRedirectWithAdvancedConversion()
|
|
||||||
{
|
|
||||||
$this->config->convertAdvanced = true;
|
|
||||||
$this->assertRequestIsOk(
|
|
||||||
'redirect',
|
|
||||||
[
|
|
||||||
'url' => 'https://www.youtube.com/watch?v=M7IpKCZ47pU',
|
|
||||||
'format' => 'best',
|
|
||||||
'customConvert' => 'on',
|
|
||||||
'customBitrate' => 32,
|
|
||||||
'customFormat' => 'flv',
|
|
||||||
],
|
|
||||||
$this->config
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test the json() function without the URL parameter.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testJsonWithoutUrl()
|
|
||||||
{
|
|
||||||
$this->assertRequestIsClientError('json');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test the json() function.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testJson()
|
|
||||||
{
|
|
||||||
$this->assertRequestIsOk('json', ['url' => 'https://www.youtube.com/watch?v=M7IpKCZ47pU']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test the json() function with an error.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testJsonWithError()
|
|
||||||
{
|
|
||||||
$this->assertRequestIsServerError('json', ['url' => 'http://example.com/foo']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the locale() function.
|
* Test the locale() function.
|
||||||
*
|
*
|
||||||
|
|
54
tests/JsonControllerTest.php
Normal file
54
tests/JsonControllerTest.php
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* JsonControllerTest class.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Alltube\Test;
|
||||||
|
|
||||||
|
use Alltube\Controller\JsonController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for the FrontController class.
|
||||||
|
*/
|
||||||
|
class JsonControllerTest extends ControllerTest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Prepare tests.
|
||||||
|
*/
|
||||||
|
protected function setUp()
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->controller = new JsonController($this->container);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the json() function.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testJson()
|
||||||
|
{
|
||||||
|
$this->assertRequestIsOk('json', ['url' => 'https://www.youtube.com/watch?v=M7IpKCZ47pU']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the json() function with an error.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testJsonWithError()
|
||||||
|
{
|
||||||
|
$this->assertRequestIsServerError('json', ['url' => 'http://example.com/foo']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the json() function without the URL parameter.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testJsonWithoutUrl()
|
||||||
|
{
|
||||||
|
$this->assertRequestIsClientError('json');
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,12 +7,11 @@ namespace Alltube\Test;
|
||||||
|
|
||||||
use Alltube\Locale;
|
use Alltube\Locale;
|
||||||
use Alltube\LocaleManager;
|
use Alltube\LocaleManager;
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit tests for the Config class.
|
* Unit tests for the LocaleManagerTest class.
|
||||||
*/
|
*/
|
||||||
class LocaleManagerTest extends TestCase
|
class LocaleManagerTest extends BaseTest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* LocaleManager class instance.
|
* LocaleManager class instance.
|
||||||
|
@ -26,19 +25,18 @@ class LocaleManagerTest extends TestCase
|
||||||
*/
|
*/
|
||||||
protected function setUp()
|
protected function setUp()
|
||||||
{
|
{
|
||||||
$this->localeManager = new LocaleManager();
|
|
||||||
$_SESSION[LocaleManager::class]['locale'] = 'foo_BAR';
|
$_SESSION[LocaleManager::class]['locale'] = 'foo_BAR';
|
||||||
|
$this->localeManager = new LocaleManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the getSupportedLocales function.
|
* Unset locale after each test.
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function testConstructorWithCookies()
|
protected function tearDown()
|
||||||
{
|
{
|
||||||
$localeManager = new LocaleManager([]);
|
$this->localeManager->unsetLocale();
|
||||||
$this->assertEquals('foo_BAR', (string) $localeManager->getLocale());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -8,7 +8,6 @@ namespace Alltube\Test;
|
||||||
use Alltube\Locale;
|
use Alltube\Locale;
|
||||||
use Alltube\LocaleManager;
|
use Alltube\LocaleManager;
|
||||||
use Alltube\LocaleMiddleware;
|
use Alltube\LocaleMiddleware;
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use Slim\Container;
|
use Slim\Container;
|
||||||
use Slim\Http\Environment;
|
use Slim\Http\Environment;
|
||||||
use Slim\Http\Request;
|
use Slim\Http\Request;
|
||||||
|
@ -17,7 +16,7 @@ use Slim\Http\Response;
|
||||||
/**
|
/**
|
||||||
* Unit tests for the FrontController class.
|
* Unit tests for the FrontController class.
|
||||||
*/
|
*/
|
||||||
class LocaleMiddlewareTest extends TestCase
|
class LocaleMiddlewareTest extends BaseTest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* LocaleMiddleware instance.
|
* LocaleMiddleware instance.
|
||||||
|
|
|
@ -6,12 +6,11 @@
|
||||||
namespace Alltube\Test;
|
namespace Alltube\Test;
|
||||||
|
|
||||||
use Alltube\Locale;
|
use Alltube\Locale;
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit tests for the Config class.
|
* Unit tests for the LocaleTest class.
|
||||||
*/
|
*/
|
||||||
class LocaleTest extends TestCase
|
class LocaleTest extends BaseTest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Locale class instance.
|
* Locale class instance.
|
||||||
|
@ -77,4 +76,14 @@ class LocaleTest extends TestCase
|
||||||
{
|
{
|
||||||
$this->assertEquals('fr', $this->localeObject->getIso3166());
|
$this->assertEquals('fr', $this->localeObject->getIso3166());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the getCountry function.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testGetCountry()
|
||||||
|
{
|
||||||
|
$this->assertEquals(country('fr'), $this->localeObject->getCountry());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,112 +5,23 @@
|
||||||
|
|
||||||
namespace Alltube\Test;
|
namespace Alltube\Test;
|
||||||
|
|
||||||
use Alltube\Config;
|
use Alltube\Stream\PlaylistArchiveStream;
|
||||||
use Alltube\PlaylistArchiveStream;
|
use Alltube\Video;
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit tests for the ViewFactory class.
|
* Unit tests for the PlaylistArchiveStream class.
|
||||||
*/
|
*/
|
||||||
class PlaylistArchiveStreamTest extends TestCase
|
class PlaylistArchiveStreamTest extends StreamTest
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* PlaylistArchiveStream instance.
|
|
||||||
*
|
|
||||||
* @var PlaylistArchiveStream
|
|
||||||
*/
|
|
||||||
private $stream;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepare tests.
|
* Prepare tests.
|
||||||
*/
|
*/
|
||||||
protected function setUp()
|
protected function setUp()
|
||||||
{
|
{
|
||||||
if (PHP_OS == 'WINNT') {
|
parent::setUp();
|
||||||
$configFile = 'config_test_windows.yml';
|
|
||||||
} else {
|
|
||||||
$configFile = 'config_test.yml';
|
|
||||||
}
|
|
||||||
$this->stream = new PlaylistArchiveStream(Config::getInstance('config/'.$configFile));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
$video = new Video('https://www.youtube.com/playlist?list=PL1j4Ff8cAqPu5iowaeUAY8lRgkfT4RybJ');
|
||||||
* Test the stream_open() function.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testStreamOpen()
|
|
||||||
{
|
|
||||||
$this->assertTrue($this->stream->stream_open('playlist://foo'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
$this->stream = new PlaylistArchiveStream($video);
|
||||||
* Test the stream_write() function.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testStreamWrite()
|
|
||||||
{
|
|
||||||
$this->assertEquals(0, $this->stream->stream_write());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test the stream_stat() function.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testStreamStat()
|
|
||||||
{
|
|
||||||
$this->assertEquals(['mode' => 4096], $this->stream->stream_stat());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test the stream_tell() function.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testStreamTell()
|
|
||||||
{
|
|
||||||
$this->stream->stream_open('playlist://foo');
|
|
||||||
$this->assertInternalType('int', $this->stream->stream_tell());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test the stream_seek() function.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testStreamSeek()
|
|
||||||
{
|
|
||||||
$this->stream->stream_open('playlist://foo');
|
|
||||||
$this->assertInternalType('bool', $this->stream->stream_seek(3));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test the stream_read() function.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testStreamRead()
|
|
||||||
{
|
|
||||||
$this->stream->stream_open('playlist://BaW_jenozKc;BaW_jenozKc/worst');
|
|
||||||
while (!$this->stream->stream_eof()) {
|
|
||||||
$result = $this->stream->stream_read(8192);
|
|
||||||
$this->assertInternalType('string', $result);
|
|
||||||
if (is_string($result)) {
|
|
||||||
$this->assertLessThanOrEqual(8192, strlen($result));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test the stream_eof() function.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testStreamEof()
|
|
||||||
{
|
|
||||||
$this->stream->stream_open('playlist://foo');
|
|
||||||
$this->assertFalse($this->stream->stream_eof());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
222
tests/StreamTest.php
Normal file
222
tests/StreamTest.php
Normal file
|
@ -0,0 +1,222 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* StreamTest class.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Alltube\Test;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class used by the stream tests.
|
||||||
|
*/
|
||||||
|
abstract class StreamTest extends BaseTest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Stream instance.
|
||||||
|
*/
|
||||||
|
protected $stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean variables used in tests.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function tearDown()
|
||||||
|
{
|
||||||
|
$this->stream->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the write() function.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testWrite()
|
||||||
|
{
|
||||||
|
if ($this->stream->isWritable()) {
|
||||||
|
$this->assertNull($this->stream->write('foo'));
|
||||||
|
} else {
|
||||||
|
$this->expectException(RuntimeException::class);
|
||||||
|
$this->stream->write('foo');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the tell() function.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testTell()
|
||||||
|
{
|
||||||
|
$this->assertInternalType('int', $this->stream->tell());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the seek() function.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testSeek()
|
||||||
|
{
|
||||||
|
if ($this->stream->isSeekable()) {
|
||||||
|
if ($this->stream->isWritable()) {
|
||||||
|
// We might need some data.
|
||||||
|
$this->stream->write('foobar');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->stream->seek(3);
|
||||||
|
$this->assertEquals(3, $this->stream->tell());
|
||||||
|
} else {
|
||||||
|
$this->expectException(RuntimeException::class);
|
||||||
|
$this->stream->seek(3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the read() function.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testRead()
|
||||||
|
{
|
||||||
|
$result = $this->stream->read(8192);
|
||||||
|
$this->assertInternalType('string', $result);
|
||||||
|
$this->assertLessThanOrEqual(8192, strlen($result));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the read() function.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testReadEntireStream()
|
||||||
|
{
|
||||||
|
$this->markTestIncomplete('Can we test the whole logic without reading the whole stream?');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the eof() function.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testEof()
|
||||||
|
{
|
||||||
|
$this->assertFalse($this->stream->eof());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the getSize() function.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testGetSize()
|
||||||
|
{
|
||||||
|
$this->assertNull($this->stream->getSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the isSeekable() function.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testIsSeekable()
|
||||||
|
{
|
||||||
|
$this->assertInternalType('boolean', $this->stream->isSeekable());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the rewind() function.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testRewind()
|
||||||
|
{
|
||||||
|
if ($this->stream->isSeekable()) {
|
||||||
|
if ($this->stream->isWritable()) {
|
||||||
|
// We might need some data.
|
||||||
|
$this->stream->write('foobar');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->stream->rewind();
|
||||||
|
$this->assertEquals(0, $this->stream->tell());
|
||||||
|
} else {
|
||||||
|
$this->expectException(RuntimeException::class);
|
||||||
|
$this->stream->rewind();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the isWritable() function.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testIsWritable()
|
||||||
|
{
|
||||||
|
$this->assertInternalType('boolean', $this->stream->isWritable());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the isReadable() function.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testIsReadable()
|
||||||
|
{
|
||||||
|
$this->assertTrue($this->stream->isReadable());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the getContents() function.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testGetContents()
|
||||||
|
{
|
||||||
|
$this->assertInternalType('string', $this->stream->getContents());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the getMetadata() function.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testGetMetadata()
|
||||||
|
{
|
||||||
|
$this->assertInternalType('array', $this->stream->getMetadata());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the getMetadata() function.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testGetMetadataWithKey()
|
||||||
|
{
|
||||||
|
$this->assertInternalType('string', $this->stream->getMetadata('stream_type'));
|
||||||
|
$this->assertInternalType('string', $this->stream->getMetadata('mode'));
|
||||||
|
$this->assertInternalType('boolean', $this->stream->getMetadata('seekable'));
|
||||||
|
$this->assertNull($this->stream->getMetadata('foo'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the detach() function.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testDetach()
|
||||||
|
{
|
||||||
|
$this->assertInternalType('resource', $this->stream->detach());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the __toString() function.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testToString()
|
||||||
|
{
|
||||||
|
$this->assertInternalType('string', $this->stream->__toString());
|
||||||
|
$this->assertInternalType('string', (string) $this->stream);
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,14 +6,13 @@
|
||||||
namespace Alltube\Test;
|
namespace Alltube\Test;
|
||||||
|
|
||||||
use Alltube\UglyRouter;
|
use Alltube\UglyRouter;
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use Slim\Http\Environment;
|
use Slim\Http\Environment;
|
||||||
use Slim\Http\Request;
|
use Slim\Http\Request;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit tests for the UglyRouter class.
|
* Unit tests for the UglyRouter class.
|
||||||
*/
|
*/
|
||||||
class UglyRouterTest extends TestCase
|
class UglyRouterTest extends BaseTest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* UglyRouter instance.
|
* UglyRouter instance.
|
||||||
|
|
|
@ -1,138 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* VideoDownloadStubsTest class.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Alltube\Test;
|
|
||||||
|
|
||||||
use Alltube\Config;
|
|
||||||
use Alltube\VideoDownload;
|
|
||||||
use Mockery;
|
|
||||||
use phpmock\mockery\PHPMockery;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unit tests for the VideoDownload class.
|
|
||||||
* They are in a separate file so they can safely replace PHP functions with stubs.
|
|
||||||
*/
|
|
||||||
class VideoDownloadStubsTest extends TestCase
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* VideoDownload instance.
|
|
||||||
*
|
|
||||||
* @var VideoDownload
|
|
||||||
*/
|
|
||||||
private $download;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Config class instance.
|
|
||||||
*
|
|
||||||
* @var Config
|
|
||||||
*/
|
|
||||||
private $config;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Video URL used in many tests.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
private $url;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize properties used by test.
|
|
||||||
*/
|
|
||||||
protected function setUp()
|
|
||||||
{
|
|
||||||
PHPMockery::mock('Alltube', 'popen');
|
|
||||||
PHPMockery::mock('Alltube', 'fopen');
|
|
||||||
|
|
||||||
if (PHP_OS == 'WINNT') {
|
|
||||||
$configFile = 'config_test_windows.yml';
|
|
||||||
} else {
|
|
||||||
$configFile = 'config_test.yml';
|
|
||||||
}
|
|
||||||
$this->config = Config::getInstance('config/'.$configFile);
|
|
||||||
$this->download = new VideoDownload($this->config);
|
|
||||||
$this->url = 'https://www.youtube.com/watch?v=XJC9_JkzugE';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove stubs.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function tearDown()
|
|
||||||
{
|
|
||||||
Mockery::close();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test getAudioStream function with a buggy popen.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
* @expectedException Exception
|
|
||||||
*/
|
|
||||||
public function testGetAudioStreamWithPopenError()
|
|
||||||
{
|
|
||||||
$this->download->getAudioStream($this->url, 'best');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test getM3uStream function with a buggy popen.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
* @expectedException Exception
|
|
||||||
*/
|
|
||||||
public function testGetM3uStreamWithPopenError()
|
|
||||||
{
|
|
||||||
$this->download->getM3uStream($this->download->getJSON($this->url, 'best'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test getRtmpStream function with a buggy popen.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
* @expectedException Exception
|
|
||||||
*/
|
|
||||||
public function testGetRtmpStreamWithPopenError()
|
|
||||||
{
|
|
||||||
$this->download->getRtmpStream($this->download->getJSON($this->url, 'best'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test getRemuxStream function with a buggy popen.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
* @expectedException Exception
|
|
||||||
*/
|
|
||||||
public function testGetRemuxStreamWithPopenError()
|
|
||||||
{
|
|
||||||
$this->download->getRemuxStream([$this->url, $this->url]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test getPlaylistArchiveStream function with a buggy popen.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
* @expectedException Exception
|
|
||||||
*/
|
|
||||||
public function testGetPlaylistArchiveStreamWithPopenError()
|
|
||||||
{
|
|
||||||
$video = $this->download->getJSON(
|
|
||||||
'https://www.youtube.com/playlist?list=PLgdySZU6KUXL_8Jq5aUkyNV7wCa-4wZsC',
|
|
||||||
'best'
|
|
||||||
);
|
|
||||||
$this->download->getPlaylistArchiveStream($video, 'best');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test getConvertedStream function with a buggy popen.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
* @expectedException Exception
|
|
||||||
*/
|
|
||||||
public function testGetConvertedStreamWithPopenError()
|
|
||||||
{
|
|
||||||
$this->download->getConvertedStream($this->url, 'best', 32, 'flv');
|
|
||||||
}
|
|
||||||
}
|
|
103
tests/VideoStubsTest.php
Normal file
103
tests/VideoStubsTest.php
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* VideoStubsTest class.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Alltube\Test;
|
||||||
|
|
||||||
|
use Alltube\Video;
|
||||||
|
use Mockery;
|
||||||
|
use phpmock\mockery\PHPMockery;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for the Video class.
|
||||||
|
* They are in a separate file so they can safely replace PHP functions with stubs.
|
||||||
|
*/
|
||||||
|
class VideoStubsTest extends BaseTest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Video URL used in many tests.
|
||||||
|
*
|
||||||
|
* @var Video
|
||||||
|
*/
|
||||||
|
private $video;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize properties used by test.
|
||||||
|
*/
|
||||||
|
protected function setUp()
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
PHPMockery::mock('Alltube', 'popen');
|
||||||
|
PHPMockery::mock('Alltube', 'fopen');
|
||||||
|
|
||||||
|
$this->video = new Video('https://www.youtube.com/watch?v=XJC9_JkzugE');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove stubs.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function tearDown()
|
||||||
|
{
|
||||||
|
Mockery::close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test getAudioStream function with a buggy popen.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
* @expectedException Exception
|
||||||
|
*/
|
||||||
|
public function testGetAudioStreamWithPopenError()
|
||||||
|
{
|
||||||
|
$this->video->getAudioStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test getM3uStream function with a buggy popen.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
* @expectedException Exception
|
||||||
|
*/
|
||||||
|
public function testGetM3uStreamWithPopenError()
|
||||||
|
{
|
||||||
|
$this->video->getM3uStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test getRtmpStream function with a buggy popen.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
* @expectedException Exception
|
||||||
|
*/
|
||||||
|
public function testGetRtmpStreamWithPopenError()
|
||||||
|
{
|
||||||
|
$this->video->getRtmpStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test getRemuxStream function with a buggy popen.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
* @expectedException Exception
|
||||||
|
*/
|
||||||
|
public function testGetRemuxStreamWithPopenError()
|
||||||
|
{
|
||||||
|
$video = $this->video->withFormat('bestvideo+bestaudio');
|
||||||
|
$video->getRemuxStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test getConvertedStream function with a buggy popen.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
* @expectedException Exception
|
||||||
|
*/
|
||||||
|
public function testGetConvertedStreamWithPopenError()
|
||||||
|
{
|
||||||
|
$this->video->getConvertedStream(32, 'flv');
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,92 +1,30 @@
|
||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* VideoDownloadTest class.
|
* VideoTest class.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace Alltube\Test;
|
namespace Alltube\Test;
|
||||||
|
|
||||||
use Alltube\Config;
|
use Alltube\Config;
|
||||||
use Alltube\VideoDownload;
|
use Alltube\Video;
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit tests for the VideoDownload class.
|
* Unit tests for the Video class.
|
||||||
*/
|
*/
|
||||||
class VideoDownloadTest extends TestCase
|
class VideoTest extends BaseTest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* VideoDownload instance.
|
* Test getExtractors function.
|
||||||
*
|
|
||||||
* @var VideoDownload
|
|
||||||
*/
|
|
||||||
private $download;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Config class instance.
|
|
||||||
*
|
|
||||||
* @var Config
|
|
||||||
*/
|
|
||||||
private $config;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize properties used by test.
|
|
||||||
*/
|
|
||||||
protected function setUp()
|
|
||||||
{
|
|
||||||
if (PHP_OS == 'WINNT') {
|
|
||||||
$configFile = 'config_test_windows.yml';
|
|
||||||
} else {
|
|
||||||
$configFile = 'config_test.yml';
|
|
||||||
}
|
|
||||||
$this->config = Config::getInstance('config/'.$configFile);
|
|
||||||
$this->download = new VideoDownload($this->config);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Destroy properties after test.
|
|
||||||
*/
|
|
||||||
protected function tearDown()
|
|
||||||
{
|
|
||||||
Config::destroyInstance();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test VideoDownload constructor with wrong youtube-dl path.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
* @expectedException Exception
|
|
||||||
*/
|
|
||||||
public function testConstructorWithMissingYoutubedl()
|
|
||||||
{
|
|
||||||
$this->config->youtubedl = 'foo';
|
|
||||||
new VideoDownload($this->config);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test VideoDownload constructor with wrong Python path.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
* @expectedException Exception
|
|
||||||
*/
|
|
||||||
public function testConstructorWithMissingPython()
|
|
||||||
{
|
|
||||||
$this->config->python = 'foo';
|
|
||||||
new VideoDownload($this->config);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test listExtractors function.
|
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function testListExtractors()
|
public function testGetExtractors()
|
||||||
{
|
{
|
||||||
$extractors = $this->download->listExtractors();
|
$this->assertContains('youtube', Video::getExtractors());
|
||||||
$this->assertContains('youtube', $extractors);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test getURL function.
|
* Test getUrl function.
|
||||||
*
|
*
|
||||||
* @param string $url URL
|
* @param string $url URL
|
||||||
* @param string $format Format
|
* @param string $format Format
|
||||||
|
@ -99,52 +37,70 @@ class VideoDownloadTest extends TestCase
|
||||||
* @dataProvider m3uUrlProvider
|
* @dataProvider m3uUrlProvider
|
||||||
* @dataProvider remuxUrlProvider
|
* @dataProvider remuxUrlProvider
|
||||||
*/
|
*/
|
||||||
public function testGetURL(
|
public function testgetUrl(
|
||||||
$url,
|
$url,
|
||||||
$format,
|
$format,
|
||||||
/* @scrutinizer ignore-unused */ $filename,
|
/* @scrutinizer ignore-unused */ $filename,
|
||||||
/* @scrutinizer ignore-unused */ $extension,
|
/* @scrutinizer ignore-unused */ $extension,
|
||||||
$domain
|
$domain
|
||||||
) {
|
) {
|
||||||
$videoURL = $this->download->getURL($url, $format);
|
$video = new Video($url, $format);
|
||||||
$this->assertContains($domain, $videoURL[0]);
|
foreach ($video->getUrl() as $videoURL) {
|
||||||
|
$this->assertContains($domain, $videoURL);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test getURL function with a protected video.
|
* Test getUrl function with a protected video.
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function testGetURLWithPassword()
|
public function testgetUrlWithPassword()
|
||||||
{
|
{
|
||||||
$videoURL = $this->download->getURL('http://vimeo.com/68375962', null, 'youtube-dl');
|
if (getenv('CI')) {
|
||||||
$this->assertContains('vimeocdn.com', $videoURL[0]);
|
$this->markTestSkipped('Travis is blacklisted by Vimeo.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$video = new Video('http://vimeo.com/68375962', 'best', 'youtube-dl');
|
||||||
|
foreach ($video->getUrl() as $videoURL) {
|
||||||
|
$this->assertContains('vimeocdn.com', $videoURL);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test getURL function with a protected video and no password.
|
* Test getUrl function with a protected video and no password.
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
* @expectedException Alltube\PasswordException
|
* @expectedException Alltube\Exception\PasswordException
|
||||||
*/
|
*/
|
||||||
public function testGetURLWithMissingPassword()
|
public function testgetUrlWithMissingPassword()
|
||||||
{
|
{
|
||||||
$this->download->getURL('http://vimeo.com/68375962');
|
if (getenv('CI')) {
|
||||||
|
$this->markTestSkipped('Travis is blacklisted by Vimeo.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$video = new Video('http://vimeo.com/68375962');
|
||||||
|
$video->getUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test getURL function with a protected video and a wrong password.
|
* Test getUrl function with a protected video and a wrong password.
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
* @expectedException Exception
|
* @expectedException Exception
|
||||||
*/
|
*/
|
||||||
public function testGetURLWithWrongPassword()
|
public function testgetUrlWithWrongPassword()
|
||||||
{
|
{
|
||||||
$this->download->getURL('http://vimeo.com/68375962', null, 'foo');
|
if (getenv('CI')) {
|
||||||
|
$this->markTestSkipped('Travis is blacklisted by Vimeo.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$video = new Video('http://vimeo.com/68375962', 'best', 'foo');
|
||||||
|
$video->getUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test getURL function errors.
|
* Test getUrl function errors.
|
||||||
*
|
*
|
||||||
* @param string $url URL
|
* @param string $url URL
|
||||||
*
|
*
|
||||||
|
@ -152,9 +108,10 @@ class VideoDownloadTest extends TestCase
|
||||||
* @expectedException Exception
|
* @expectedException Exception
|
||||||
* @dataProvider ErrorUrlProvider
|
* @dataProvider ErrorUrlProvider
|
||||||
*/
|
*/
|
||||||
public function testGetURLError($url)
|
public function testgetUrlError($url)
|
||||||
{
|
{
|
||||||
$this->download->getURL($url);
|
$video = new Video($url);
|
||||||
|
$video->getUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -164,7 +121,7 @@ class VideoDownloadTest extends TestCase
|
||||||
*/
|
*/
|
||||||
public function urlProvider()
|
public function urlProvider()
|
||||||
{
|
{
|
||||||
return [
|
$videos = [
|
||||||
[
|
[
|
||||||
'https://www.youtube.com/watch?v=M7IpKCZ47pU', 'best[protocol^=http]',
|
'https://www.youtube.com/watch?v=M7IpKCZ47pU', 'best[protocol^=http]',
|
||||||
'It_s_Not_Me_It_s_You_-_Hearts_Under_Fire-M7IpKCZ47pU',
|
'It_s_Not_Me_It_s_You_-_Hearts_Under_Fire-M7IpKCZ47pU',
|
||||||
|
@ -178,12 +135,6 @@ class VideoDownloadTest extends TestCase
|
||||||
'mp4',
|
'mp4',
|
||||||
'googlevideo.com',
|
'googlevideo.com',
|
||||||
],
|
],
|
||||||
[
|
|
||||||
'https://vimeo.com/24195442', 'best[protocol^=http]',
|
|
||||||
'Carving_the_Mountains-24195442',
|
|
||||||
'mp4',
|
|
||||||
'vimeocdn.com',
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
'http://www.bbc.co.uk/programmes/b039g8p7', 'bestaudio/best',
|
'http://www.bbc.co.uk/programmes/b039g8p7', 'bestaudio/best',
|
||||||
'Kaleidoscope_Leonard_Cohen-b039d07m',
|
'Kaleidoscope_Leonard_Cohen-b039d07m',
|
||||||
|
@ -203,6 +154,18 @@ class VideoDownloadTest extends TestCase
|
||||||
'openload.co',
|
'openload.co',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (!getenv('CI')) {
|
||||||
|
// Travis is blacklisted by Vimeo.
|
||||||
|
$videos[] = [
|
||||||
|
'https://vimeo.com/24195442', 'best[protocol^=http]',
|
||||||
|
'Carving_the_Mountains-24195442',
|
||||||
|
'mp4',
|
||||||
|
'vimeocdn.com',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $videos;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -229,16 +192,21 @@ class VideoDownloadTest extends TestCase
|
||||||
*/
|
*/
|
||||||
public function m3uUrlProvider()
|
public function m3uUrlProvider()
|
||||||
{
|
{
|
||||||
return [
|
$videos = [];
|
||||||
[
|
|
||||||
|
if (!getenv('CI')) {
|
||||||
|
// Twitter returns a 429 error when the test is ran too many times.
|
||||||
|
$videos[] = [
|
||||||
'https://twitter.com/verge/status/813055465324056576/video/1', 'hls-2176',
|
'https://twitter.com/verge/status/813055465324056576/video/1', 'hls-2176',
|
||||||
'The_Verge_-_This_tiny_origami_robot_can_self-fold_and_complete_tasks-813055465324056576',
|
'The_Verge_-_This_tiny_origami_robot_can_self-fold_and_complete_tasks-813055465324056576',
|
||||||
'mp4',
|
'mp4',
|
||||||
'video.twimg.com',
|
'video.twimg.com',
|
||||||
],
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $videos;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides RTMP URLs for tests.
|
* Provides RTMP URLs for tests.
|
||||||
*
|
*
|
||||||
|
@ -278,9 +246,10 @@ class VideoDownloadTest extends TestCase
|
||||||
* @dataProvider urlProvider
|
* @dataProvider urlProvider
|
||||||
* @dataProvider m3uUrlProvider
|
* @dataProvider m3uUrlProvider
|
||||||
*/
|
*/
|
||||||
public function testGetJSON($url, $format)
|
public function testGetJson($url, $format)
|
||||||
{
|
{
|
||||||
$info = $this->download->getJSON($url, $format);
|
$video = new Video($url, $format);
|
||||||
|
$info = $video->getJson();
|
||||||
$this->assertObjectHasAttribute('webpage_url', $info);
|
$this->assertObjectHasAttribute('webpage_url', $info);
|
||||||
$this->assertObjectHasAttribute('url', $info);
|
$this->assertObjectHasAttribute('url', $info);
|
||||||
$this->assertObjectHasAttribute('ext', $info);
|
$this->assertObjectHasAttribute('ext', $info);
|
||||||
|
@ -298,9 +267,10 @@ class VideoDownloadTest extends TestCase
|
||||||
* @expectedException Exception
|
* @expectedException Exception
|
||||||
* @dataProvider ErrorURLProvider
|
* @dataProvider ErrorURLProvider
|
||||||
*/
|
*/
|
||||||
public function testGetJSONError($url)
|
public function testGetJsonError($url)
|
||||||
{
|
{
|
||||||
$this->download->getJSON($url);
|
$video = new Video($url);
|
||||||
|
$video->getJson();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -318,8 +288,8 @@ class VideoDownloadTest extends TestCase
|
||||||
*/
|
*/
|
||||||
public function testGetFilename($url, $format, $filename, $extension)
|
public function testGetFilename($url, $format, $filename, $extension)
|
||||||
{
|
{
|
||||||
$videoFilename = $this->download->getFilename($url, $format);
|
$video = new Video($url, $format);
|
||||||
$this->assertEquals($videoFilename, $filename.'.'.$extension);
|
$this->assertEquals($video->getFilename(), $filename.'.'.$extension);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -333,25 +303,8 @@ class VideoDownloadTest extends TestCase
|
||||||
*/
|
*/
|
||||||
public function testGetFilenameError($url)
|
public function testGetFilenameError($url)
|
||||||
{
|
{
|
||||||
$this->download->getFilename($url);
|
$video = new Video($url);
|
||||||
}
|
$video->getFilename();
|
||||||
|
|
||||||
/**
|
|
||||||
* Test getAudioFilename function.
|
|
||||||
*
|
|
||||||
* @param string $url URL
|
|
||||||
* @param string $format Format
|
|
||||||
* @param string $filename Filename
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
* @dataProvider urlProvider
|
|
||||||
* @dataProvider m3uUrlProvider
|
|
||||||
* @dataProvider remuxUrlProvider
|
|
||||||
*/
|
|
||||||
public function testGetAudioFilename($url, $format, $filename)
|
|
||||||
{
|
|
||||||
$videoFilename = $this->download->getAudioFilename($url, $format);
|
|
||||||
$this->assertEquals($videoFilename, $filename.'.mp3');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -365,9 +318,8 @@ class VideoDownloadTest extends TestCase
|
||||||
*/
|
*/
|
||||||
public function testGetAudioStream($url, $format)
|
public function testGetAudioStream($url, $format)
|
||||||
{
|
{
|
||||||
$stream = $this->download->getAudioStream($url, $format);
|
$video = new Video($url, $format);
|
||||||
$this->assertInternalType('resource', $stream);
|
$this->assertStream($video->getAudioStream());
|
||||||
$this->assertFalse(feof($stream));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -382,9 +334,10 @@ class VideoDownloadTest extends TestCase
|
||||||
*/
|
*/
|
||||||
public function testGetAudioStreamAvconvError($url, $format)
|
public function testGetAudioStreamAvconvError($url, $format)
|
||||||
{
|
{
|
||||||
$this->config->avconv = 'foobar';
|
Config::setOptions(['avconv' => 'foobar']);
|
||||||
$download = new VideoDownload($this->config);
|
|
||||||
$download->getAudioStream($url, $format);
|
$video = new Video($url, $format);
|
||||||
|
$video->getAudioStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -399,7 +352,8 @@ class VideoDownloadTest extends TestCase
|
||||||
*/
|
*/
|
||||||
public function testGetAudioStreamM3uError($url, $format)
|
public function testGetAudioStreamM3uError($url, $format)
|
||||||
{
|
{
|
||||||
$this->download->getAudioStream($url, $format);
|
$video = new Video($url, $format);
|
||||||
|
$video->getAudioStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -410,7 +364,12 @@ class VideoDownloadTest extends TestCase
|
||||||
*/
|
*/
|
||||||
public function testGetAudioStreamDashError()
|
public function testGetAudioStreamDashError()
|
||||||
{
|
{
|
||||||
$this->download->getAudioStream('https://vimeo.com/251997032', 'bestaudio/best');
|
if (getenv('CI')) {
|
||||||
|
$this->markTestSkipped('Travis is blacklisted by Vimeo.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$video = new Video('https://vimeo.com/251997032', 'bestaudio/best');
|
||||||
|
$video->getAudioStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -421,10 +380,11 @@ class VideoDownloadTest extends TestCase
|
||||||
*/
|
*/
|
||||||
public function testGetAudioStreamPlaylistError()
|
public function testGetAudioStreamPlaylistError()
|
||||||
{
|
{
|
||||||
$this->download->getAudioStream(
|
$video = new Video(
|
||||||
'https://www.youtube.com/playlist?list=PLgdySZU6KUXL_8Jq5aUkyNV7wCa-4wZsC',
|
'https://www.youtube.com/playlist?list=PLgdySZU6KUXL_8Jq5aUkyNV7wCa-4wZsC',
|
||||||
'best'
|
'best'
|
||||||
);
|
);
|
||||||
|
$video->getAudioStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -451,11 +411,8 @@ class VideoDownloadTest extends TestCase
|
||||||
*/
|
*/
|
||||||
public function testGetM3uStream($url, $format)
|
public function testGetM3uStream($url, $format)
|
||||||
{
|
{
|
||||||
$this->assertStream(
|
$video = new Video($url, $format);
|
||||||
$this->download->getM3uStream(
|
$this->assertStream($video->getM3uStream());
|
||||||
$this->download->getJSON($url, $format)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -469,10 +426,24 @@ class VideoDownloadTest extends TestCase
|
||||||
*/
|
*/
|
||||||
public function testGetRemuxStream($url, $format)
|
public function testGetRemuxStream($url, $format)
|
||||||
{
|
{
|
||||||
$urls = $this->download->getURL($url, $format);
|
$video = new Video($url, $format);
|
||||||
if (count($urls) > 1) {
|
$this->assertStream($video->getRemuxStream());
|
||||||
$this->assertStream($this->download->getRemuxStream($urls));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test getRemuxStream function with a video with only one URL.
|
||||||
|
*
|
||||||
|
* @param string $url URL
|
||||||
|
* @param string $format Format
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
* @dataProvider urlProvider
|
||||||
|
* @expectedException Exception
|
||||||
|
*/
|
||||||
|
public function testGetRemuxStreamWithWrongVideo($url, $format)
|
||||||
|
{
|
||||||
|
$video = new Video($url, $format);
|
||||||
|
$video->getRemuxStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -488,11 +459,9 @@ class VideoDownloadTest extends TestCase
|
||||||
{
|
{
|
||||||
$this->markTestIncomplete('We need to find another RTMP video.');
|
$this->markTestIncomplete('We need to find another RTMP video.');
|
||||||
|
|
||||||
$this->assertStream(
|
$video = new Video($url, $format);
|
||||||
$this->download->getRtmpStream(
|
|
||||||
$this->download->getJSON($url, $format)
|
$this->assertStream($video->getRtmpStream());
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -507,25 +476,10 @@ class VideoDownloadTest extends TestCase
|
||||||
*/
|
*/
|
||||||
public function testGetM3uStreamAvconvError($url, $format)
|
public function testGetM3uStreamAvconvError($url, $format)
|
||||||
{
|
{
|
||||||
$this->config->avconv = 'foobar';
|
Config::setOptions(['avconv' => 'foobar']);
|
||||||
$download = new VideoDownload($this->config);
|
|
||||||
$video = $download->getJSON($url, $format);
|
|
||||||
$download->getM3uStream($video);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
$video = new Video($url, $format);
|
||||||
* Test getPlaylistArchiveStream function.
|
$video->getM3uStream();
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
* @requires OS Linux
|
|
||||||
*/
|
|
||||||
public function testGetPlaylistArchiveStream()
|
|
||||||
{
|
|
||||||
$video = $this->download->getJSON(
|
|
||||||
'https://www.youtube.com/playlist?list=PLgdySZU6KUXL_8Jq5aUkyNV7wCa-4wZsC',
|
|
||||||
'best'
|
|
||||||
);
|
|
||||||
$this->assertStream($this->download->getPlaylistArchiveStream($video, 'best'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -539,7 +493,8 @@ class VideoDownloadTest extends TestCase
|
||||||
*/
|
*/
|
||||||
public function testGetConvertedStream($url, $format)
|
public function testGetConvertedStream($url, $format)
|
||||||
{
|
{
|
||||||
$this->assertStream($this->download->getConvertedStream($url, $format, 32, 'flv'));
|
$video = new Video($url, $format);
|
||||||
|
$this->assertStream($video->getConvertedStream(32, 'flv'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -554,6 +509,7 @@ class VideoDownloadTest extends TestCase
|
||||||
*/
|
*/
|
||||||
public function testGetConvertedStreamM3uError($url, $format)
|
public function testGetConvertedStreamM3uError($url, $format)
|
||||||
{
|
{
|
||||||
$this->download->getConvertedStream($url, $format, 32, 'flv');
|
$video = new Video($url, $format);
|
||||||
|
$video->getConvertedStream(32, 'flv');
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -6,7 +6,6 @@
|
||||||
namespace Alltube\Test;
|
namespace Alltube\Test;
|
||||||
|
|
||||||
use Alltube\ViewFactory;
|
use Alltube\ViewFactory;
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use Slim\Container;
|
use Slim\Container;
|
||||||
use Slim\Http\Environment;
|
use Slim\Http\Environment;
|
||||||
use Slim\Http\Request;
|
use Slim\Http\Request;
|
||||||
|
@ -15,7 +14,7 @@ use Slim\Views\Smarty;
|
||||||
/**
|
/**
|
||||||
* Unit tests for the ViewFactory class.
|
* Unit tests for the ViewFactory class.
|
||||||
*/
|
*/
|
||||||
class ViewFactoryTest extends TestCase
|
class ViewFactoryTest extends BaseTest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Test the create() function.
|
* Test the create() function.
|
||||||
|
|
27
tests/YoutubeChunkStreamTest.php
Normal file
27
tests/YoutubeChunkStreamTest.php
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* YoutubeChunkStreamTest class.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Alltube\Test;
|
||||||
|
|
||||||
|
use Alltube\Stream\YoutubeChunkStream;
|
||||||
|
use Alltube\Video;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for the YoutubeChunkStream class.
|
||||||
|
*/
|
||||||
|
class YoutubeChunkStreamTest extends StreamTest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Prepare tests.
|
||||||
|
*/
|
||||||
|
protected function setUp()
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$video = new Video('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
|
||||||
|
|
||||||
|
$this->stream = new YoutubeChunkStream($video->getHttpResponse());
|
||||||
|
}
|
||||||
|
}
|
47
tests/YoutubeStreamTest.php
Normal file
47
tests/YoutubeStreamTest.php
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* YoutubeStreamTest class.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Alltube\Test;
|
||||||
|
|
||||||
|
use Alltube\Stream\YoutubeStream;
|
||||||
|
use Alltube\Video;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for the YoutubeStream class.
|
||||||
|
*/
|
||||||
|
class YoutubeStreamTest extends StreamTest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Prepare tests.
|
||||||
|
*/
|
||||||
|
protected function setUp()
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$video = new Video('https://www.youtube.com/watch?v=dQw4w9WgXcQ', '135');
|
||||||
|
|
||||||
|
$this->stream = new YoutubeStream($video);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the getMetadata() function.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testGetMetadataWithKey()
|
||||||
|
{
|
||||||
|
$this->assertNull($this->stream->getMetadata('foo'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the detach() function.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testDetach()
|
||||||
|
{
|
||||||
|
$this->assertNull($this->stream->detach());
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,6 @@
|
||||||
/**
|
/**
|
||||||
* File used to bootstrap tests.
|
* File used to bootstrap tests.
|
||||||
*/
|
*/
|
||||||
use Alltube\PlaylistArchiveStream;
|
|
||||||
use phpmock\mockery\PHPMockery;
|
use phpmock\mockery\PHPMockery;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -14,8 +13,6 @@ ini_set('session.use_cookies', 0);
|
||||||
session_cache_limiter('');
|
session_cache_limiter('');
|
||||||
session_start();
|
session_start();
|
||||||
|
|
||||||
stream_wrapper_register('playlist', PlaylistArchiveStream::class);
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* @see https://bugs.php.net/bug.php?id=68541
|
* @see https://bugs.php.net/bug.php?id=68541
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in a new issue