From d7927fc442be35528d20ec2652679b75c70000a3 Mon Sep 17 00:00:00 2001
From: Pierre Rudloff
Date: Tue, 2 May 2017 17:04:55 +0200
Subject: [PATCH] Download Tar archives from playlists
---
classes/PlaylistArchiveStream.php | 200 ++++++++++++++++++++++++++++
classes/VideoDownload.php | 19 +++
composer.json | 3 +-
composer.lock | 42 +++++-
controllers/FrontController.php | 15 ++-
index.php | 3 +
templates/playlist.tpl | 3 +
tests/FrontControllerTest.php | 25 ++++
tests/PlaylistArchiveStreamTest.php | 98 ++++++++++++++
tests/VideoDownloadTest.php | 11 ++
tests/bootstrap.php | 3 +
11 files changed, 419 insertions(+), 3 deletions(-)
create mode 100644 classes/PlaylistArchiveStream.php
create mode 100644 tests/PlaylistArchiveStreamTest.php
diff --git a/classes/PlaylistArchiveStream.php b/classes/PlaylistArchiveStream.php
new file mode 100644
index 0000000..6e00747
--- /dev/null
+++ b/classes/PlaylistArchiveStream.php
@@ -0,0 +1,200 @@
+client = new \GuzzleHttp\Client();
+ $this->download = new VideoDownload();
+ }
+
+ /**
+ * Add data to the archive.
+ *
+ * @param mixed $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)
+ * @param string $mode Stream mode
+ *
+ * @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 mixed
+ */
+ 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..5d4fb68 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 string $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..dd4a7d0 100644
--- a/controllers/FrontController.php
+++ b/controllers/FrontController.php
@@ -317,7 +317,16 @@ 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()) {
@@ -426,6 +435,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}