diff --git a/classes/PlaylistArchiveStream.php b/classes/PlaylistArchiveStream.php new file mode 100644 index 0000000..ee89c4d --- /dev/null +++ b/classes/PlaylistArchiveStream.php @@ -0,0 +1,199 @@ +client = new \GuzzleHttp\Client(); + $this->download = new VideoDownload(); + } + + /** + * Add data to the archive. + * + * @param string $data Data + * + * @return void + */ + protected function send($data) + { + $pos = ftell($this->buffer); + fwrite($this->buffer, $data); + 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), '/'); + $this->buffer = fopen('php://temp', 'r+'); + 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 + */ + 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 + */ + public function stream_read($count) + { + if (!$this->files[$this->curFile]['headersSent']) { + $urls = $this->download->getUrl($this->files[$this->curFile]['url'], $this->format); + $response = $this->client->request('GET', $urls[0], ['stream' => true]); + + $contentLengthHeaders = $response->getHeader('Content-Length'); + $this->init_file_stream_transfer( + $this->download->getFilename($this->files[$this->curFile]['url'], $this->format), + $contentLengthHeaders[0] + ); + + $this->files[$this->curFile]['headersSent'] = true; + $this->files[$this->curFile]['stream'] = $response->getBody(); + } elseif (!$this->files[$this->curFile]['stream']->eof()) { + $this->stream_file_part($this->files[$this->curFile]['stream']->read($count)); + } elseif (!$this->files[$this->curFile]['complete']) { + $this->complete_file_stream(); + $this->files[$this->curFile]['complete'] = true; + } elseif (isset($this->files[$this->curFile])) { + $this->curFile += 1; + } + + return fread($this->buffer, $count); + } +} diff --git a/classes/VideoDownload.php b/classes/VideoDownload.php index 92ee07d..1348333 100644 --- a/classes/VideoDownload.php +++ b/classes/VideoDownload.php @@ -364,4 +364,23 @@ class VideoDownload { return popen($this->getRtmpProcess($video)->getCommandLine(), 'r'); } + + /** + * 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 + * + * @return Response HTTP response + */ + public function getPlaylistArchiveStream($video, $format) + { + $playlistItems = []; + foreach ($video->entries as $entry) { + $playlistItems[] = urlencode($entry->url); + } + $stream = fopen('playlist://'.implode(';', $playlistItems).'/'.$format, 'r'); + + return $stream; + } } diff --git a/composer.json b/composer.json index a3b50cb..2d844c2 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,8 @@ "ptachoire/process-builder-chain": "~1.2.0", "rudloff/smarty-plugin-noscheme": "~0.1.0", "guzzlehttp/guzzle": "~6.2.0", - "aura/session": "~2.1.0" + "aura/session": "~2.1.0", + "barracudanetworks/archivestream-php": "~1.0.5" }, "require-dev": { "symfony/var-dumper": "~3.2.0", diff --git a/composer.lock b/composer.lock index 3a4e674..5f1474b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "18563e36d487153b121da01e2cf040db", + "content-hash": "6f968a7d8b884758ea4655dd80f0697d", "packages": [ { "name": "aura/session", @@ -68,6 +68,46 @@ ], "time": "2016-10-03T20:28:32+00:00" }, + { + "name": "barracudanetworks/archivestream-php", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/barracudanetworks/ArchiveStream-php.git", + "reference": "1bf98097d1e9b137fd40081f26abb0a17b097ef7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barracudanetworks/ArchiveStream-php/zipball/1bf98097d1e9b137fd40081f26abb0a17b097ef7", + "reference": "1bf98097d1e9b137fd40081f26abb0a17b097ef7", + "shasum": "" + }, + "require": { + "ext-gmp": "*", + "ext-mbstring": "*", + "php": ">=5.1.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Barracuda\\ArchiveStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A library for dynamically streaming dynamic tar or zip files without the need to have the complete file stored on the server.", + "homepage": "https://github.com/barracudanetworks/ArchiveStream-php", + "keywords": [ + "archive", + "php", + "stream", + "tar", + "zip" + ], + "time": "2017-01-13T14:52:38+00:00" + }, { "name": "container-interop/container-interop", "version": "1.2.0", diff --git a/controllers/FrontController.php b/controllers/FrontController.php index d68f205..bbcc358 100644 --- a/controllers/FrontController.php +++ b/controllers/FrontController.php @@ -317,26 +317,32 @@ class FrontController private function getStream($url, $format, Response $response, Request $request, $password = null) { $video = $this->download->getJSON($url, $format, $password); - if ($video->protocol == 'rtmp') { + 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); - if ($request->isGet()) { - $response = $response->withBody(new Stream($stream)); - } + $body = new Stream($stream); } elseif ($video->protocol == 'm3u8') { $stream = $this->download->getM3uStream($video); $response = $response->withHeader('Content-Type', 'video/'.$video->ext); - if ($request->isGet()) { - $response = $response->withBody(new Stream($stream)); - } + $body = new Stream($stream); } else { $client = new \GuzzleHttp\Client(); $stream = $client->request('GET', $video->url, ['stream' => true]); $response = $response->withHeader('Content-Type', $stream->getHeader('Content-Type')); $response = $response->withHeader('Content-Length', $stream->getHeader('Content-Length')); - if ($request->isGet()) { - $response = $response->withBody($stream->getBody()); - } + $body = $stream->getBody(); + } + if ($request->isGet()) { + $response = $response->withBody($body); } $response = $response->withHeader( 'Content-Disposition', @@ -350,7 +356,7 @@ class FrontController /** * Get a remuxed stream piped through the server. * - * @param array $urls URLs of the video and audio files + * @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 @@ -426,6 +432,10 @@ class FrontController $this->sessionSegment->getFlash($url) ); } else { + if (empty($videoUrls[0])) { + throw new \Exception("Can't find URL of video"); + } + return $response->withRedirect($videoUrls[0]); } } diff --git a/index.php b/index.php index 0bb2c6e..3b55687 100644 --- a/index.php +++ b/index.php @@ -3,6 +3,7 @@ require_once __DIR__.'/vendor/autoload.php'; use Alltube\Config; use Alltube\Controller\FrontController; +use Alltube\PlaylistArchiveStream; use Alltube\UglyRouter; use Alltube\ViewFactory; use Slim\App; @@ -12,6 +13,8 @@ if (isset($_SERVER['REQUEST_URI']) && strpos($_SERVER['REQUEST_URI'], '/index.ph die; } +stream_wrapper_register('playlist', PlaylistArchiveStream::class); + $app = new App(); $container = $app->getContainer(); $config = Config::getInstance(); diff --git a/templates/playlist.tpl b/templates/playlist.tpl index f6c9f22..40f3669 100644 --- a/templates/playlist.tpl +++ b/templates/playlist.tpl @@ -6,6 +6,9 @@ {$video->title}{/if} playlist:

+{if $config->stream} + webpage_url}" class="downloadBtn">Download everything +{/if} {foreach $video->entries as $video}