Youtube Download Klasse
Youtube download class
This class is an easy to use wrapper around the commandline programm youtube-dl
,
which must be installed on your (*nix) system. See the example how to use it.
Diese Klasse ist um das Kommandozeilenprogramm youtube-dl
gestrickt, welches auf dem
(*nix) System installiert sein muss. Ein Blick in das nachfolgende Beispiel verrät
wie es funktioniert:
Sample source code
Anwendungsbeispiel
<?php
include_once('swlib/swlib.class.php');
use sw\YoutubeDownloader;
// Class configuration. This config is used for all instances.
sw\YoutubeDownloader::config(array(
'youtube-dl' => '/usr/bin/youtube-dl',
'default-download-directory' => '/tmp'
));
// This is the clip url
$uri = 'https://www.youtube.com/watch?v=BYBw_o_2nG0';
// Instantiate
$ytdl = new YoutubeDownloader($uri);
// Get the title from youtube
$title = $ytdl->getTitle();
// Get Description form youtube
$description = $ytdl->getDescription();
// Get all available formats from youttube
$formats = $ytdl->getAvailableFormats();
// Your progress callback
$ytdl->setProgressCallback(function($progress) {
//
// $progress['progress'] : progress from 0.0 to 1.0
// $progress['total'] => : total bytes that have to be loaded
// $progress['speed'] => : download speed in bytes/second
// $progress['remaining']: time remaining in seconds
//
print " -> {$progress['progress']}, {$progress['remaining']}\n";
});
print "\n";
print "Title : $title\n\n";
print "Description: $description\n\n";
print "Formats :";
print sprintf(" %3s | %10s | %10s | %10s\n", 'id', 'type', 'width', 'height');
foreach($formats as $format) {
print sprintf(" %10s | %10s | %10s | %10s\n", $format['id'],
$format['type'], $format['width'], $format['height']);
}
print "\n";
foreach($formats as $format) {
if($format['type'] != 'mp4') continue;
if($format['width'] != 640) continue;
$ytdl->download($format['id']);
break;
}
Output
Ausgabe
$ php ytdl.test.php
Title : Despicable Me - Mini-Movie 'Banana' Preview
Description : No description available.
Formats : id | type | width | height
35 | flv | 854 | 480
44 | webm | 854 | 480
34 | flv | 640 | 360
18 | mp4 | 640 | 360
43 | webm | 640 | 360
5 | flv | 400 | 240
17 | mp4 | 176 | 144
-> 0, 16
-> 0, 20
-> 0.001, 19
-> 0.002, 22
-> 0.004, 20
-> 0.008, 18
-> 0.016, 17
-> 0.031, 17
-> 0.063, 16
-> 0.121, 15
-> 0.179, 14
-> 0.237, 13
-> 0.295, 12
-> 0.353, 11
-> 0.411, 10
-> 0.469, 9
-> 0.527, 8
-> 0.585, 7
-> 0.643, 6
-> 0.701, 5
-> 0.759, 4
-> 0.817, 3
-> 0.875, 2
-> 0.933, 1
-> 0.991, 0
-> 1, 0
Class source code
Klassen-Quelltext
<?php
/**
* Exceptions thrown by class YoutubeDownloader
*
* @gpackage de.atwillys.sw.php.swLib
* @author Stefan Wilhelm
* @copyright Stefan Wilhelm, 2007-2012
* @license GPL
* @version 1.0
*/
namespace sw;
class YoutubeDownloaderException extends LException {
}
<?php
/**
* Wrapper for the youtube-dl command line program. Allows to download streams
* and meta information.
*
* @gpackage de.atwillys.sw.php.swLib
* @author Stefan Wilhelm
* @copyright Stefan Wilhelm, 2007-2012
* @license GPL
* @version 1.0
*/
namespace sw;
class YoutubeDownloader {
/**
* Class configuration
* @var array
*/
protected static $config = array(
'youtube-dl' => '/usr/bin/youtube-dl',
'default-download-directory' => '/tmp'
);
/**
* The output video file path
* @var string
*/
protected $outputFile = null;
/**
* The uri to fetch data or streams from
* @var string
*/
protected $pageUri = null;
/**
* The video title
* @var string
*/
protected $title = null;
/**
* The video description text
* @var string
*/
protected $description = null;
/**
* The used video format
* @var string
*/
protected $format = null;
/**
* Formats the video is available in
* @var array
*/
protected $formats = array();
/**
* The process callback function reference, can be left null
* @var function
*/
protected $progressCallback = null;
/**
* Returns the class configuration. If a configuration array is given, modifies
* the configuration by key merging.
* @param array $config
* @return array
*/
public static final function config(array $config = array()) {
if (!empty($config)) {
self::$config = array_merge(self::$config, $config);
Tracer::trace_r($config, '$config', 3);
}
if (empty(self::$config['youtube-dl']) || !FileSystem::isFile(self::$config['youtube-dl']) || !FileSystem::isExecutable(self::$config['youtube-dl'])) {
$ytdl = trim(exec('which youtube-dl'), "\t\n\r ");
Tracer::trace("youtube-dl binary search result (which youtube-dl)=$ytdl", 2);
if (empty($ytdl)) {
throw new YoutubeDownloaderException("Your configutation is incorrect: can't find the youtube-dl binary: :binary", array(':binary' => self::$config['youtube-dl']));
} else {
Tracer::trace("Warning: The configured mimms binary path is wrong (" . self::$config['youtube-dl'] . "), but found binary '$ytdl'");
}
self::$config['youtube-dl'] = $ytdl;
}
return self::$config;
}
/**
* Constructor
* @param string $uri
*/
public function __construct($uri = null) {
if (!empty($uri)) {
$this->pageUri = $uri;
try {
$this->updateProperties();
} catch (\Exception $e) {
// Don't interrupt the object construction
print "$e";
}
}
}
/**
* Sets the progress callback function reference
* @param function $callback
*/
public function setProgressCallback($callback) {
if (!is_callable($callback)) {
throw new YoutubeDownloaderException('Your progress callback is not callable.');
} else {
$this->progressCallback = $callback;
}
}
/**
* Returns output file path
* @return string
*/
public function getOutputFile() {
return $this->outputFile;
}
/**
* Returns the title
* @return string
*/
public function getTitle() {
$this->updateProperties();
return $this->title;
}
/**
* Returns the video description text
* @return string
*/
public function getDescription() {
$this->updateProperties();
return $this->description;
}
/**
* Returns the downloaded format
* @return string
*/
public function getFormat() {
$this->updateProperties();
return $this->format;
}
/**
* Returns the available formats on youtube
* @return array
*/
public function getAvailableFormats() {
$this->updateProperties();
return $this->formats;
}
/**
* Shell process on STDOUT callback, class internal use
* @param string $text
*/
public final function onStdOut($text) {
if (preg_match("/\[download\]\s+([\d\.]+)\%[\s]+of[\s]+([\d\.]+[\s]*[\w]+)[\s]+at[\s]+([\d\.]+[\s]*[\w\/]+)[\s]+ETA[\s]+([\d\:]+)/i", $text, $matches)) {
if (isset($this->progressCallback)) {
$progress = array(
'progress' => floatval($matches[1]) / 100,
'loaded' => 0,
'total' => $this->sizeToBytes($matches[2]),
'speed' => $this->sizeToBytes($matches[3]),
'remaining' => $this->timeToSeconds($matches[4])
);
$progress['loaded'] = intval($progress['progress'] * $progress['total']);
@call_user_func($this->progressCallback, $progress);
}
}
}
/**
* Converts string data size formats to SI (e.g. 100K to 100000)
* @param string $text
* @return int
*/
protected function sizeToBytes($text) {
if (preg_match('/([\d\.]+)[\s]*([\w]*)/i', $text, $matches)) {
array_shift($matches);
$n = doubleval(reset($matches));
$u = strtolower(trim(end($matches)));
if (strlen($u) > 1)
$u = substr($u, 0, 1);
switch ($u) {
case 't': $n *= 1024;
case 'g': $n *= 1024;
case 'm': $n *= 1024;
case 'k': $n *= 1024;
}
return intval($n);
} else {
return false;
}
}
/**
* Converts a time duration string (e.g. "01:45:00.0") to seconds
* @param string $timeString
* @return int
*/
protected function timeToSeconds($timeString) {
$timeString = explode(':', $timeString);
$t = 0;
foreach (array_reverse($timeString) as $k => $v) {
$t += $v * pow(60, $k);
}
return $t;
}
/**
* Updates the obkect instance variables by the given uri.
*/
protected function updateProperties() {
if (is_null($this->title)) {
if (stripos($this->pageUri, '://www.youtube.com') === false) {
throw new YoutubeDownloaderException('The given url does contain :page', array(':page', 'www.youtube.com'));
} else {
$r = ShellProcess::exec(self::$config['youtube-dl'] . ' --get-title ' . escapeshellarg($this->pageUri));
if (!empty($r['stderr'])) {
throw new YoutubeDownloaderException($r['stderr']);
} else {
$this->title = trim($r['stdout'], "\n\r\t ");
}
$r = ShellProcess::exec(self::$config['youtube-dl'] . ' --get-description ' . escapeshellarg($this->pageUri));
if (!empty($r['stderr'])) {
throw new YoutubeDownloaderException($r['stderr']);
} else {
$this->description = trim($r['stdout'], "\n\r\t ");
}
$r = ShellProcess::exec(self::$config['youtube-dl'] . ' --list-formats ' . escapeshellarg($this->pageUri));
if (!empty($r['stderr'])) {
throw new YoutubeDownloaderException($r['stderr']);
} else {
$this->formats = array();
$this->format = null;
$actual_width = 0;
foreach (explode("\n", trim($r['stdout'], "\n\r\t ")) as $format) {
if (preg_match("/^(\d+)[\s\:]+(\w+)\s*\[(\d+)x(\d+)\]/i", $format, $matches)) {
$format = array(
'id' => $matches[1],
'type' => $matches[2],
'height' => $matches[3],
'width' => $matches[4],
);
$this->formats[] = $format;
// Use best quality format in mp4 as default.
if ($format['type'] == 'mp4' && $actual_width < $format['width']) {
$this->format = $format['id'];
$actual_width = $format['width'];
}
}
}
// Default format if no matching mp4
if (!empty($this->formats) && is_null($this->format)) {
$this->format = $this->formats[0]['id'];
}
}
}
}
}
/**
* Downloads the video file and returns a reference to itself.
* @param int $format
* @return YoutubeDownloader
*/
public function download($format=null) {
$this->updateProperties();
if (!empty($format)) {
$found = false;
foreach ($this->formats as $f) {
if ($f['id'] == $format) {
$found = true;
break;
}
}
if (!$found) {
throw new YoutubeDownloaderException("Did not find this format (:format) in the list of formats", array(':format' => $format));
} else if (!is_numeric($format)) {
throw new YoutubeDownloaderException("Video format codes must be numeric, this one is not: :format", array(':format' => $format));
} else {
$this->format = $format;
}
unset($found);
unset($f);
}
if (!FileSystem::isDirectory(self::$config['default-download-directory'])) {
throw new YoutubeDownloaderException("Download directory does not exist: :directory", array(':directory' => self::$config['default-download-directory']));
}
// Get file name
$r = ShellProcess::exec(self::$config['youtube-dl'] . ' -f ' . $this->format . ' --get-filename ' . escapeshellarg($this->pageUri));
if (!empty($r['stderr'])) {
throw new YoutubeDownloaderException($r['stderr']);
} else {
$this->outputFile = trim($r['stdout'], "\n\r\t /");
if (empty($this->outputFile)) {
throw new YoutubeDownloaderException('The downloader returned no output file name to use.');
} else {
$this->outputFile = self::$config['default-download-directory'] . '/youtube-' . time() . '-' . $this->outputFile;
}
}
// Download
$sh = new ShellProcess();
$sh->setCommand(self::$config['youtube-dl'] . ' --no-part -f ' . $this->format . ' -o ' . escapeshellarg($this->outputFile) . ' ' . escapeshellarg($this->pageUri));
$sh->setFetchOutput(true);
$sh->setCallbacks(array('onstdout' => array($this, 'onStdOut')));
$sh->run();
if (strlen($sh->getStdErr()) > 0) {
throw new YoutubeDownloaderException($r['stderr']);
}
return $this;
}
}