<?php
\n
\n
\n
\nclass Torrent {
\n\t
\n\t/**
\n\t* @const float Default http timeout
\n\t*/
\n\tconst timeout = 30;
\n
\n\t/**
\n\t* @var array List of error occurred
\n\t*/
\n\tstatic protected $_errors = array();
\n
\n\t/** Read and decode torrent file/data OR build a torrent from source folder/file(s)
\n\t * Supported signatures:
\n\t * - Torrent(); // get an instance (useful to scrape and check errors)
\n\t * - Torrent( string $torrent ); // analyze a torrent file
\n\t * - Torrent( string $torrent, string $announce );
\n\t * - Torrent( string $torrent, array $meta );
\n\t * - Torrent( string $file_or_folder ); // create a torrent file
\n\t * - Torrent( string $file_or_folder, string $announce_url, [int $piece_length] );
\n\t * - Torrent( string $file_or_folder, array $meta, [int $piece_length] );
\n\t * - Torrent( array $files_list );
\n\t * - Torrent( array $files_list, string $announce_url, [int $piece_length] );
\n\t * - Torrent( array $files_list, array $meta, [int $piece_length] );
\n\t * @param string|array torrent to read or source folder/file(s) (optional, to get an instance)
\n\t * @param string|array announce url or meta informations (optional)
\n\t * @param int piece length (optional)
\n\t */
\n\tpublic function __construct ( $data = null, $meta = array(), $piece_length = 256 ) {
\n\t\tif ( is_null( $data ) )
\n\t\t\treturn false;
\n\t\tif ( $piece_length < 32 || $piece_length > 4096 )
\n\t\t\treturn self::set_error( new Exception( 'Invalid piece length, must be between 32 and 4096' ) );
\n\t\tif ( is_string( $meta ) )
\n\t\t\t$meta = array( 'announce' => $meta );
\n\t\tif ( $this->build( $data, $piece_length * 1024 ) )
\n\t\t\t$this->touch();
\n\t\telse
\n\t\t\t$meta = array_merge( $meta, $this->decode( $data ) );
\n\t\tforeach( $meta as $key => $value )
\n\t\t\t$this->{$key} = $value;
\n\t}
\n
\n\t/** Convert the current Torrent instance in torrent format
\n\t * @return string encoded torrent data
\n\t */
\n\tpublic function __toString() {
\n\t\treturn $this->encode( $this );
\n\t}
\n
\n\t/** Return last error message
\n\t * @return string|boolean last error message or false if none
\n\t */
\n\tpublic function error() {
\n\t\treturn empty( self::$_errors ) ?
\n\t\t\tfalse :
\n\t\t\tself::$_errors[0]->getMessage();
\n\t}
\n
\n\t/** Return Errors
\n\t * @return array|boolean error list or false if none
\n\t */
\n\tpublic function errors() {
\n\t\treturn empty( self::$_errors ) ?
\n\t\t\tfalse :
\n\t\t\tself::$_errors;
\n\t}
\n
\n\t/**** Getters and setters ****/
\n
\n\t/** Getter and setter of torrent announce url / list
\n\t * If the argument is a string, announce url is added to announce list (or set as announce if announce is not set)
\n\t * If the argument is an array/object, set announce url (with first url) and list (if array has more than one url), tiered list supported
\n\t * If the argument is false announce url & list are unset
\n\t * @param null|false|string|array announce url / list, reset all if false (optional, if omitted it's a getter)
\n\t * @return string|array|null announce url / list or null if not set
\n\t */
\n\tpublic function announce ( $announce = null ) {
\n\t\tif ( is_null( $announce ) )
\n\t\t\treturn ! isset( $this->{'announce-list'} ) ?
\n\t\t\t\tisset( $this->announce ) ? $this->announce : null :
\n\t\t\t\t$this->{'announce-list'};
\n\t\t$this->touch();
\n\t\tif ( is_string( $announce ) && isset( $this->announce ) )
\n\t\t\treturn $this->{'announce-list'} = self::announce_list( isset( $this->{'announce-list'} ) ? $this->{'announce-list'} : $this->announce, $announce );
\n\t\tunset( $this->{'announce-list'} );
\n\t\tif ( is_array( $announce ) || is_object( $announce ) )
\n\t\t\tif ( ( $this->announce = self::first_announce( $announce ) ) && count( $announce ) > 1 )
\n\t\t\t\treturn $this->{'announce-list'} = self::announce_list( $announce );
\n\t\t\telse
\n\t\t\t\treturn $this->announce;
\n\t\tif ( ! isset( $this->announce ) && $announce )
\n\t\t\treturn $this->announce = (string) $announce;
\n\t\tunset( $this->announce );
\n\t}
\n
\n\t/** Getter and setter of torrent creation date
\n\t * @param null|integer timestamp (optional, if omitted it's a getter)
\n\t * @return integer|null timestamp or null if not set
\n\t */
\n\tpublic function creation_date ( $timestamp = null ) {
\n\t\treturn is_null( $timestamp ) ?
\n\t\t\tisset( $this->{'creation date'} ) ? $this->{'creation date'} : null :
\n\t\t\t$this->touch( $this->{'creation date'} = (int) $timestamp );
\n\t}
\n\t
\n\t/** Getter and setter of torrent comment
\n\t * @param null|string comment (optional, if omitted it's a getter)
\n\t * @return string|null comment or null if not set
\n\t */
\n\tpublic function comment ( $comment = null ) {
\n\t\treturn is_null( $comment ) ?
\n\t\t\tisset( $this->comment ) ? $this->comment : null :
\n\t\t\t$this->touch( $this->comment = (string) $comment );
\n\t}
\n
\n\t/** Getter and setter of torrent name
\n\t * @param null|string name (optional, if omitted it's a getter)
\n\t * @return string|null name or null if not set
\n\t */
\n\tpublic function name ( $name = null ) {
\n\t\treturn is_null( $name ) ?
\n\t\t\tisset( $this->info['name'] ) ? $this->info['name'] : null :
\n\t\t\t$this->touch( $this->info['name'] = (string) $name );
\n\t}
\n
\n\t/** Getter and setter of private flag
\n\t * @param null|boolean is private or not (optional, if omitted it's a getter)
\n\t * @return boolean private flag
\n\t */
\n\tpublic function is_private ( $private = null ) {
\n\t\treturn is_null( $private ) ?
\n\t\t\t! empty( $this->info['private'] ) :
\n\t\t\t$this->touch( $this->info['private'] = $private ? 1 : 0 );
\n\t}
\n
\n\t/** Getter and setter of torrent source
\n\t * @param null|string source (optional, if omitted it's a getter)
\n\t * @return string|null source or null if not set
\n\t */
\n\tpublic function source ( $source = null ) {
\n\t\treturn is_null( $source ) ?
\n\t\t\tisset( $this->info['source'] ) ? $this->info['source'] : null :
\n\t\t\t$this->touch( $this->info['source'] = (string) $source );
\n
\n\t}
\n
\n\t/** Getter and setter of webseed(s) url list ( GetRight implementation )
\n\t * @param null|string|array webseed or webseeds mirror list (optional, if omitted it's a getter)
\n\t * @return string|array|null webseed(s) or null if not set
\n\t */
\n\tpublic function url_list ( $urls = null ) {
\n\t\treturn is_null( $urls ) ?
\n\t\t\tisset( $this->{'url-list'} ) ? $this->{'url-list'} : null :
\n\t\t\t$this->touch( $this->{'url-list'} = is_string( $urls) ? $urls : (array) $urls );
\n\t}
\n
\n\t/** Getter and setter of httpseed(s) url list ( BitTornado implementation )
\n\t * @param null|string|array httpseed or httpseeds mirror list (optional, if omitted it's a getter)
\n\t * @return array|null httpseed(s) or null if not set
\n\t */
\n\tpublic function httpseeds ( $urls = null ) {
\n\t\treturn is_null( $urls ) ?
\n\t\t\tisset( $this->httpseeds ) ? $this->httpseeds : null :
\n\t\t\t$this->touch( $this->httpseeds = (array) $urls );
\n\t}
\n
\n\t/**** Analyze BitTorrent ****/
\n
\n\t/** Get piece length
\n\t * @return integer piece length or null if not set
\n\t */
\n\tpublic function piece_length () {
\n\t\treturn isset( $this->info['piece length'] ) ?
\n\t\t\t$this->info['piece length'] :
\n\t\t\tnull;
\n\t}
\n
\n\t/** Compute hash info
\n\t * @return string hash info or null if info not set
\n\t */
\n\tpublic function hash_info () {
\n\t\treturn isset( $this->info ) ?
\n\t\t\tsha1( self::encode( $this->info ) ) :
\n\t\t\tnull;
\n\t}
\n
\n\t/** List torrent content
\n\t * @param integer|null size precision (optional, if omitted returns sizes in bytes)
\n\t * @return array file(s) and size(s) list, files as keys and sizes as values
\n\t */
\n\tpublic function content ( $precision = null ) {
\n\t\t$files = array();
\n\t\tif ( isset( $this->info['files'] ) && is_array( $this->info['files'] ) )
\n\t\t\tforeach ( $this->info['files'] as $file )
\n\t\t\t\t$files[self::path( $file['path'], $this->info['name'] )] = $precision ?
\n\t\t\t\t\tself::format( $file['length'], $precision ) :
\n\t\t\t\t\t$file['length'];
\n\t\telseif ( isset( $this->info['name'] ) )
\n\t\t\t\t$files[$this->info['name']] = $precision ?
\n\t\t\t\t\tself::format( $this->info['length'], $precision ) :
\n\t\t\t\t\t$this->info['length'];
\n\t\treturn $files;
\n\t}
\n
\n\t/** List torrent content pieces and offset(s)
\n\t * @return array file(s) and pieces/offset(s) list, file(s) as keys and pieces/offset(s) as values
\n\t */
\n\tpublic function offset () {
\n\t\t$files = array();
\n\t\t$size = 0;
\n\t\tif ( isset( $this->info['files'] ) && is_array( $this->info['files'] ) )
\n\t\t\tforeach ( $this->info['files'] as $file )
\n\t\t\t\t$files[self::path( $file['path'], $this->info['name'] )] = array(
\n\t\t\t\t\t'startpiece'\t=> floor( $size / $this->info['piece length'] ),
\n\t\t\t\t\t'offset'\t=> fmod( $size, $this->info['piece length'] ),
\n\t\t\t\t\t'size'\t\t=> $size += $file['length'],
\n\t\t\t\t\t'endpiece'\t=> floor( $size / $this->info['piece length'] )
\n\t\t\t\t);
\n\t\telseif ( isset( $this->info['name'] ) )
\n\t\t\t\t$files[$this->info['name']] = array(
\n\t\t\t\t\t'startpiece'\t=> 0,
\n\t\t\t\t\t'offset'\t=> 0,
\n\t\t\t\t\t'size'\t\t=> $this->info['length'],
\n\t\t\t\t\t'endpiece'\t=> floor( $this->info['length'] / $this->info['piece length'] )
\n\t\t\t\t);
\n\t\treturn $files;
\n\t}
\n
\n\t/** Sum torrent content size
\n\t * @param integer|null size precision (optional, if omitted returns size in bytes)
\n\t * @return integer|string file(s) size
\n\t */
\n\tpublic function size ( $precision = null ) {
\n\t\t$size = 0;
\n\t\tif ( isset( $this->info['files'] ) && is_array( $this->info['files'] ) )
\n\t\t\tforeach ( $this->info['files'] as $file )
\n\t\t\t\t$size += $file['length'];
\n\t\telseif ( isset( $this->info['name'] ) )
\n\t\t\t\t$size = $this->info['length'];
\n\t\treturn is_null( $precision ) ?
\n\t\t\t$size :
\n\t\t\tself::format( $size, $precision );
\n\t}
\n
\n\t/** Request torrent statistics from scrape page USING CURL!!
\n\t * @param string|array announce or scrape page url (optional, to request an alternative tracker BUT required for static call)
\n\t * @param string torrent hash info (optional, required ONLY for static call)
\n\t * @param float read timeout in seconds (optional, default to self::timeout 30s)
\n\t * @return array tracker torrent statistics
\n\t */
\n\t/* static */ public function scrape ( $announce = null, $hash_info = null, $timeout = self::timeout ) {
\n\t\t$packed_hash = urlencode( pack('H*', $hash_info ? $hash_info : $this->hash_info() ) );
\n\t\t$handles = $scrape = array();
\n\t\tif ( ! function_exists( 'curl_multi_init' ) )
\n\t\t\treturn self::set_error( new Exception( 'Install CURL with "curl_multi_init" enabled' ) );
\n\t\t$curl = curl_multi_init();
\n\t\tforeach ( (array) ($announce ? $announce : $this->announce()) as $tier )
\n\t\t\tforeach ( (array) $tier as $tracker ) {
\n\t\t\t\t$tracker = str_ireplace( array( 'udp://', '/announce', ':80/' ), array( 'http://', '/scrape', '/' ), $tracker );
\n\t\t\t\tif ( isset( $handles[$tracker] ) )
\n\t\t\t\t\tcontinue;
\n\t\t\t\t$handles[$tracker] = curl_init( $tracker . '?info_hash=' . $packed_hash );
\n\t\t\t\tcurl_setopt( $handles[$tracker], CURLOPT_RETURNTRANSFER, true );
\n\t\t\t\tcurl_setopt( $handles[$tracker], CURLOPT_TIMEOUT, $timeout );
\n\t\t\t\tcurl_multi_add_handle( $curl, $handles[$tracker] );
\n\t\t\t}
\n\t\tdo {
\n\t\t\twhile ( ( $state = curl_multi_exec( $curl, $running ) ) == CURLM_CALL_MULTI_PERFORM );
\n\t\t\t\tif( $state != CURLM_OK )
\n\t\t\t\t\tcontinue;
\n\t\t\twhile ( $done = curl_multi_info_read( $curl ) ) {
\n\t\t\t\t$info = curl_getinfo( $done['handle'] );
\n\t\t\t\t$tracker = explode( '?', $info['url'], 2 );
\n\t\t\t\t$tracker = array_shift( $tracker );
\n\t\t\t\tif ( empty( $info['http_code'] ) ) {
\n\t\t\t\t\t$scrape[$tracker] = self::set_error( new Exception( 'Tracker request timeout (' . $timeout . 's)' ), true );
\n\t\t\t\t\tcontinue;
\n\t\t\t\t} elseif ( $info['http_code'] != 200 ) {
\n\t\t\t\t\t$scrape[$tracker] = self::set_error( new Exception( 'Tracker request failed (' . $info['http_code'] . ' code)' ), true );
\n\t\t\t\t\tcontinue;
\n\t\t\t\t}
\n\t\t\t\t$data = curl_multi_getcontent( $done['handle'] );
\n\t\t\t\t$stats = self::decode_data( $data );
\n\t\t\t\tcurl_multi_remove_handle( $curl, $done['handle'] );
\n\t\t\t\t$scrape[$tracker] = empty( $stats['files'] ) ?
\n\t\t\t\t\tself::set_error( new Exception( 'Empty scrape data' ), true ) :
\n\t\t\t\t\tarray_shift( $stats['files'] ) + ( empty( $stats['flags'] ) ? array() : $stats['flags'] );
\n\t\t\t}
\n\t\t} while ( $running );
\n\t\tcurl_multi_close( $curl );
\n\t\treturn $scrape;
\n\t}
\n
\n\t/**** Save and Send ****/
\n
\n\t/** Save torrent file to disk
\n\t * @param null|string name of the file (optional)
\n\t * @return boolean file has been saved or not
\n\t */
\n\tpublic function save ( $filename = null ) {
\n\t\treturn file_put_contents( is_null( $filename ) ? $this->info['name'] . '.torrent' : $filename, $this->encode( $this ) );
\n\t}
\n
\n\t/** Send torrent file to client
\n\t * @param null|string name of the file (optional)
\n\t * @return void script exit
\n\t */
\n\tpublic function send ( $filename = null ) {
\n\t\t$data = $this->encode( $this );
\n\t\theader( 'Content-type: application/x-bittorrent' );
\n\t\theader( 'Content-Length: ' . strlen( $data ) );
\n\t\theader( 'Content-Disposition: attachment; filename="' . ( is_null( $filename ) ? $this->info['name'] . '.torrent' : $filename ) . '"' );
\n\t\texit( $data );
\n\t}
\n
\n\t/** Get magnet link
\n\t * @param boolean html encode ampersand, default true (optional)
\n\t * @return string magnet link
\n\t */
\n\tpublic function magnet ( $html = true ) {
\n\t\t$ampersand = $html ? '&amp;' : '&';
\n\t\treturn sprintf( 'magnet:?xt=urn:btih:%2$s%1$sdn=%3$s%1$sxl=%4$d%1$str=%5$s', $ampersand, $this->hash_info(), urlencode( $this->name() ), $this->size(), implode( $ampersand .'tr=', self::untier( $this->announce() ) ) );
\n\t}\t
\n
\n\t/**** Encode BitTorrent ****/
\n
\n\t/** Encode torrent data
\n\t * @param mixed data to encode
\n\t * @return string torrent encoded data
\n\t */
\n\tstatic public function encode ( $mixed ) {
\n\t\tswitch ( gettype( $mixed ) ) {
\n\t\t\tcase 'integer':
\n\t\t\tcase 'double':
\n\t\t\t\treturn self::encode_integer( $mixed );
\n\t\t\tcase 'object':
\n\t\t\t\t$mixed = get_object_vars( $mixed );
\n\t\t\tcase 'array':
\n\t\t\t\treturn self::encode_array( $mixed );
\n\t\t\tdefault:
\n\t\t\t\treturn self::encode_string( (string) $mixed );
\n\t\t}
\n\t}
\n
\n\t/** Encode torrent string
\n\t * @param string string to encode
\n\t * @return string encoded string
\n\t */
\n\tstatic private function encode_string ( $string ) {
\n\t\treturn strlen( $string ) . ':' . $string;
\n\t}
\n
\n\t/** Encode torrent integer
\n\t * @param integer integer to encode
\n\t * @return string encoded integer
\n\t */
\n\tstatic private function encode_integer ( $integer ) {
\n\t\treturn 'i' . $integer . 'e';
\n\t}
\n
\n\t/** Encode torrent dictionary or list
\n\t * @param array array to encode
\n\t * @return string encoded dictionary or list
\n\t */
\n\tstatic private function encode_array ( $array ) {
\n\t\tif ( self::is_list( $array ) ) {
\n\t\t\t$return = 'l';
\n\t\t\tforeach ( $array as $value )
\n\t\t\t\t$return .= self::encode( $value );
\n\t\t} else {
\n\t\t\tksort( $array, SORT_STRING );
\n\t\t\t$return = 'd';
\n\t\t\tforeach ( $array as $key => $value )
\n\t\t\t\t$return .= self::encode( strval( $key ) ) . self::encode( $value );
\n\t\t}
\n\t\treturn $return . 'e';
\n\t}
\n
\n\t/**** Decode BitTorrent ****/
\n
\n\t/** Decode torrent data or file
\n\t * @param string data or file path to decode
\n\t * @return array decoded torrent data
\n\t */
\n\tstatic protected function decode ( $string ) {
\n\t\t$data = is_file( $string ) || self::url_exists( $string ) ?
\n\t\t\tself::file_get_contents( $string ) :
\n\t\t\t$string;
\n\t\treturn (array) self::decode_data( $data );
\n\t}
\n
\n\t/** Decode torrent data
\n\t * @param string data to decode
\n\t * @return array decoded torrent data
\n\t */
\n\tstatic private function decode_data ( & $data ) {
\n\t\tswitch( self::char( $data ) ) {
\n\t\tcase 'i':
\n\t\t\t$data = substr( $data, 1 );
\n\t\t\treturn self::decode_integer( $data );
\n\t\tcase 'l':
\n\t\t\t$data = substr( $data, 1 );
\n\t\t\treturn self::decode_list( $data );
\n\t\tcase 'd':
\n\t\t\t$data = substr( $data, 1 );
\n\t\t\treturn self::decode_dictionary( $data );
\n\t\tdefault:
\n\t\t\treturn self::decode_string( $data );
\n\t\t}
\n\t}
\n
\n\t/** Decode torrent dictionary
\n\t * @param string data to decode
\n\t * @return array decoded dictionary
\n\t */
\n\tstatic private function decode_dictionary ( & $data ) {
\n\t\t$dictionary = array();
\n\t\t$previous = null;
\n\t\twhile ( ( $char = self::char( $data ) ) != 'e' ) {
\n\t\t\tif ( $char === false )
\n\t\t\t\treturn self::set_error( new Exception( 'Unterminated dictionary' ) );
\n\t\t\tif ( ! ctype_digit( $char ) )
\n\t\t\t\treturn self::set_error( new Exception( 'Invalid dictionary key' ) );
\n\t\t\t$key = self::decode_string( $data );
\n\t\t\tif ( isset( $dictionary[$key] ) )
\n\t\t\t\treturn self::set_error( new Exception( 'Duplicate dictionary key' ) );
\n\t\t\tif ( $key < $previous )
\n\t\t\t\tself::set_error( new Exception( 'Missorted dictionary key' ) );
\n\t\t\t$dictionary[$key] = self::decode_data( $data );
\n\t\t\t$previous = $key;
\n\t\t}
\n\t\t$data = substr( $data, 1 );
\n\t\treturn $dictionary;
\n\t}
\n
\n\t/** Decode torrent list
\n\t * @param string data to decode
\n\t * @return array decoded list
\n\t */
\n\tstatic private function decode_list ( & $data ) {
\n\t\t$list = array();
\n\t\twhile ( ( $char = self::char( $data ) ) != 'e' ) {
\n\t\t\tif ( $char === false )
\n\t\t\t\treturn self::set_error( new Exception( 'Unterminated list' ) );
\n\t\t\t$list[] = self::decode_data( $data );
\n\t\t}
\n\t\t$data = substr( $data, 1 );
\n\t\treturn $list;
\n\t}
\n
\n\t/** Decode torrent string
\n\t * @param string data to decode
\n\t * @return string decoded string
\n\t */
\n\tstatic private function decode_string ( & $data ) {
\n\t\tif ( self::char( $data ) === '0' && substr( $data, 1, 1 ) != ':' )
\n\t\t\tself::set_error( new Exception( 'Invalid string length, leading zero' ) );
\n\t\tif ( ! $colon = @strpos( $data, ':' ) )
\n\t\t\treturn self::set_error( new Exception( 'Invalid string length, colon not found' ) );
\n\t\t$length = intval( substr( $data, 0, $colon ) );
\n\t\tif ( $length + $colon + 1 > strlen( $data ) )
\n\t\t\treturn self::set_error( new Exception( 'Invalid string, input too short for string length' ) );
\n\t\t$string = substr( $data, $colon + 1, $length );
\n\t\t$data = substr( $data, $colon + $length + 1 );
\n\t\treturn $string;
\n\t}
\n
\n\t/** Decode torrent integer
\n\t * @param string data to decode
\n\t * @return integer decoded integer
\n\t */
\n\tstatic private function decode_integer ( & $data ) {
\n\t\t$start = 0;
\n\t\t$end   = strpos( $data, 'e');
\n\t\tif ( $end === 0 )
\n\t\t\tself::set_error( new Exception( 'Empty integer' ) );
\n\t\tif ( self::char( $data ) == '-' )
\n\t\t\t$start++;
\n\t\tif ( substr( $data, $start, 1 ) == '0' && $end > $start + 1 )
\n\t\t\tself::set_error( new Exception( 'Leading zero in integer' ) );
\n\t\tif ( ! ctype_digit( substr( $data, $start, $start ? $end - 1 : $end ) ) )
\n\t\t\tself::set_error( new Exception( 'Non-digit characters in integer' ) );
\n\t\t$integer = substr( $data, 0, $end );
\n\t\t$data    = substr( $data, $end + 1 );
\n\t\treturn 0 + $integer;
\n\t}
\n
\n\t/**** Internal Helpers ****/
\n
\n\t/** Build torrent info
\n\t * @param string|array source folder/file(s) path
\n\t * @param integer piece length
\n\t * @return array|boolean torrent info or false if data isn't folder/file(s)
\n\t */
\n\tprotected function build ( $data, $piece_length ) {
\n\t\tif ( is_null( $data ) )
\n\t\t\treturn false;
\n\t\telseif ( is_array( $data ) && self::is_list( $data ) )
\n\t\t\treturn $this->info = $this->files( $data, $piece_length );
\n\t\telseif ( is_dir( $data ) )
\n\t\t\treturn $this->info = $this->folder( $data, $piece_length );
\n\t\telseif ( ( is_file( $data ) || self::url_exists( $data ) ) && ! self::is_torrent( $data ) )
\n\t\t\treturn $this->info = $this->file( $data, $piece_length );
\n\t\telse
\n\t\t\treturn false;
\n\t}
\n
\n\t/** Set torrent creator and creation date
\n\t * @param any param
\n\t * @return any param
\n\t */
\n\tprotected function touch ( $void = null ) {
\n\t\t$this->{'created by'}\t\t= 'YggTorrent';
\n\t\t$this->{'creation date'}\t= time();
\n\t\treturn $void;
\n\t}
\n
\n\t/** Add an error to errors stack
\n\t * @param Exception error to add
\n\t * @param boolean return error message or not (optional, default to false)
\n\t * @return boolean|string return false or error message if requested
\n\t */
\n\tstatic protected function set_error ( $exception, $message = false ) {
\n\t\treturn ( array_unshift( self::$_errors, $exception ) && $message ) ? $exception->getMessage() : false;
\n\t}
\n
\n\t/** Build announce list
\n\t * @param string|array announce url / list
\n\t * @param string|array announce url / list to add (optionnal)
\n\t * @return array announce list (array of arrays)
\n\t */
\n\tstatic protected function announce_list( $announce, $merge = array() ) {
\n\t\treturn array_map( create_function( '$a', 'return (array) $a;' ), array_merge( (array) $announce, (array) $merge ) );
\n\t}
\n
\n\t/** Get the first announce url in a list
\n\t * @param array announce list (array of arrays if tiered trackers)
\n\t * @return string first announce url
\n\t */
\n\tstatic protected function first_announce( $announce ) {
\n\t\twhile ( is_array( $announce ) )
\n\t\t\t$announce = reset( $announce );
\n\t\treturn $announce;
\n\t}
\n
\n\t/** Helper to pack data hash
\n\t * @param string data
\n\t * @return string packed data hash
\n\t */
\n\tstatic protected function pack ( & $data ) {
\n\t\treturn pack('H*', sha1( $data ) ) . ( $data = null );
\n\t}
\n
\n\t/** Helper to build file path
\n\t * @param array file path
\n\t * @param string base folder
\n\t * @return string real file path
\n\t */
\n\tstatic protected function path ( $path, $folder ) {
\n\t\tarray_unshift( $path, $folder );
\n\t\treturn join( DIRECTORY_SEPARATOR, $path );
\n\t}
\n
\n\t/** Helper to explode file path
\n\t * @param string file path
\n\t * @return array file path
\n\t */
\n\tstatic protected function path_explode ( $path ) {
\n\t\treturn explode( DIRECTORY_SEPARATOR, $path );
\n\t}
\n
\n\t/** Helper to test if an array is a list
\n\t * @param array array to test
\n\t * @return boolean is the array a list or not
\n\t */
\n\tstatic protected function is_list ( $array ) {
\n\t\tforeach ( array_keys( $array ) as $key )
\n\t\t\tif ( ! is_int( $key ) )
\n\t\t\t\treturn false;
\n\t\treturn true;
\n\t}
\n
\n\t/** Build pieces depending on piece length from a file handler
\n\t * @param ressource file handle
\n\t * @param integer piece length
\n\t * @param boolean is last piece
\n\t * @return string pieces
\n\t */
\n\tprivate function pieces ( $handle, $piece_length, $last = true ) {
\n\t\tstatic $piece, $length;
\n\t\tif ( empty( $length ) )
\n\t\t\t$length\t= $piece_length;
\n\t\t$pieces = null;
\n\t\twhile ( ! feof( $handle ) ) {
\n\t\t\tif ( ( $length = strlen( $piece .= fread( $handle, $length ) ) ) == $piece_length )
\n\t\t\t\t$pieces .= self::pack( $piece );
\n\t\t\telseif ( ( $length = $piece_length - $length ) < 0 )
\n\t\t\t\treturn self::set_error( new Exception( 'Invalid piece length!' ) );
\n\t\t}
\n\t\tfclose( $handle );
\n\t\treturn $pieces . ( $last && $piece ? self::pack( $piece ) : null);
\n\t}
\n
\n\t/** Build torrent info from single file
\n\t * @param string file path
\n\t * @param integer piece length
\n\t * @return array torrent info
\n\t */
\n\tprivate function file ( $file, $piece_length ) {
\n\t\tif ( ! $handle = self::fopen( $file, $size = self::filesize( $file ) ) )
\n\t\t\treturn self::set_error( new Exception( 'Failed to open file: "' . $file . '"' ) );
\n\t\tif ( self::is_url( $file ) )
\n\t\t\t$this->url_list( $file );
\n\t\t$path = self::path_explode( $file );
\n\t\treturn array(
\n\t\t\t'length'\t=> $size,
\n\t\t\t'name'\t\t=> end( $path ),
\n\t\t\t'piece length'\t=> $piece_length,
\n\t\t\t'pieces'\t=> $this->pieces( $handle, $piece_length )
\n\t\t);
\n\t}
\n
\n\t/** Build torrent info from files
\n\t * @param array file list
\n\t * @param integer piece length
\n\t * @return array torrent info
\n\t */
\n\tprivate function files ( $files, $piece_length ) {
\n\t\tsort( $files );
\n\t\tusort( $files, create_function( '$a,$b', 'return strrpos($a,DIRECTORY_SEPARATOR)-strrpos($b,DIRECTORY_SEPARATOR);' ) );
\n\t\t$first\t= current( $files );
\n\t\tif ( ! self::is_url( $first ) )
\n\t\t\t$files = array_map( 'realpath', $files );
\n\t\telse
\n\t\t\t$this->url_list( dirname( $first ) . DIRECTORY_SEPARATOR );
\n\t\t$files_path = array_map('self::path_explode', $files );
\n\t\t$root = call_user_func_array('array_intersect_assoc' , $files_path);
\n\t\t$pieces = null; $info_files = array(); $count = count( $files ) - 1;
\n\t\tforeach ( $files as $i => $file ) {
\n\t\t\tif ( ! $handle = self::fopen( $file, $filesize = self::filesize( $file ) ) ) {
\n\t\t\t\tself::set_error( new Exception( 'Failed to open file: "' . $file . '" discarded' ) );
\n\t\t\t\tcontinue;
\n\t\t\t}
\n\t\t\t$pieces .= $this->pieces( $handle, $piece_length, $count == $i );
\n\t\t\t$info_files[] = array(
\n\t\t\t\t'length'\t=> $filesize,
\n\t\t\t\t'path'\t\t=> array_diff_assoc( $files_path[$i], $root )
\n\t\t\t);
\n\t\t}
\n\t\treturn array(
\n\t\t\t'files'\t\t=> $info_files,
\n\t\t\t'name'\t\t=> end( $root ),
\n\t\t\t'piece length'\t=> $piece_length,
\n\t\t\t'pieces'\t=> $pieces
\n\t\t);
\n
\n\t}
\n
\n\t/** Build torrent info from folder content
\n\t * @param string folder path
\n\t * @param integer piece length
\n\t * @return array torrent info
\n\t */
\n\tprivate function folder ( $dir, $piece_length ) {
\n\t\treturn $this->files( self::scandir( $dir ), $piece_length );
\n\t}
\n
\n\t/** Helper to return the first char of encoded data
\n\t * @param string encoded data
\n\t * @return string|boolean first char of encoded data or false if empty data
\n\t */
\n\tstatic private function char ( $data ) {
\n\t\treturn empty( $data ) ?
\n\t\t\tfalse :
\n\t\t\tsubstr( $data, 0, 1 );
\n\t}
\n
\n\t/**** Public Helpers ****/
\n
\n\t/** Helper to format size in bytes to human readable
\n\t * @param integer size in bytes
\n\t * @param integer precision after coma
\n\t * @return string formated size in appropriate unit
\n\t */
\n\tstatic public function format ( $size, $precision = 2 ) {
\n\t\t$units = array ('octets', 'Ko', 'Mo', 'Go', 'To');
\n\t\twhile( ( $next = next( $units ) ) && $size > 1024 )
\n\t\t\t$size /= 1024;
\n\t\treturn round( $size, $precision ) . ' ' . ( $next ? prev( $units ) : end( $units ) );
\n\t}
\n
\n\t/** Helper to return filesize (even bigger than 2Gb -linux only- and distant files size)
\n\t * @param string file path
\n\t * @return double|boolean filesize or false if error
\n\t */
\n\tstatic public function filesize ( $file ) {
\n\t\tif ( is_file( $file ) )
\n\t\t\treturn (double) sprintf( '%u', @filesize( $file ) );
\n\t\telse if ( $content_length = preg_grep( $pattern = '#^Content-Length:\\s+(\\d+)$#i', (array) @get_headers( $file ) ) )
\n\t\t\treturn (int) preg_replace( $pattern, '$1', reset( $content_length ) );
\n\t}
\n
\n\t/** Helper to open file to read (even bigger than 2Gb, linux only)
\n\t * @param string file path
\n\t * @param integer|double file size (optional)
\n\t * @return resource|boolean file handle or false if error
\n\t */
\n\tstatic public function fopen ( $file, $size = null ) {
\n\t\tif ( ( is_null( $size ) ? self::filesize( $file ) : $size ) <= 2 * pow( 1024, 3 ) )
\n\t\t\treturn fopen( $file, 'r' );
\n\t\telseif ( PHP_OS != 'Linux' )
\n\t\t\treturn self::set_error( new Exception( 'File size is greater than 2GB. This is only supported under Linux' ) );
\n\t\telseif ( ! is_readable( $file ) )
\n\t\t\treturn false;
\n\t\telse
\n\t\t\treturn popen( 'cat ' . escapeshellarg( realpath( $file ) ), 'r' );
\n\t}
\n
\n\t/** Helper to scan directories files and sub directories recursively
\n\t * @param string directory path
\n\t * @return array directory content list
\n\t */
\n\tstatic public function scandir ( $dir ) {
\n\t\t$paths = array();
\n\t\tforeach ( scandir( $dir ) as $item )
\n\t\t\t\tif ( $item != '.' && $item != '..' )
\n\t\t\t\t\tif ( is_dir( $path = realpath( $dir . DIRECTORY_SEPARATOR . $item ) ) )
\n\t\t\t\t\t\t$paths = array_merge( self::scandir( $path ), $paths );
\n\t\t\t\t\telse
\n\t\t\t\t\t\t$paths[] = $path;
\n\t\treturn $paths;
\n\t}
\n
\n\t/** Helper to check if string is an url (http)
\n\t * @param string url to check
\n\t * @return boolean is string an url
\n\t */
\n\tstatic public function is_url ( $url ) {
\n\t\treturn preg_match( '#^http(s)?://[a-z0-9-]+(.[a-z0-9-]+)*(:[0-9]+)?(/.*)?$#i', $url );
\n\t}
\n
\n\t/** Helper to check if url exists
\n\t * @param string url to check
\n\t * @return boolean does the url exist or not
\n\t */
\n\tstatic public function url_exists ( $url ) {
\n\t\treturn self::is_url( $url ) ?
\n\t\t\t(bool) self::filesize ( $url ) :
\n\t\t\tfalse;
\n\t}
\n\t/** Helper to check if a file is a torrent
\n\t * @param string file location
\n\t * @param float http timeout (optional, default to self::timeout 30s)
\n\t * @return boolean is the file a torrent or not
\n\t */
\n\tstatic public function is_torrent ( $file, $timeout = self::timeout ) {
\n\t\treturn ( $start = self::file_get_contents( $file, $timeout, 0, 11 ) )
\n\t\t\t && $start === 'd8:announce'
\n\t\t\t || $start === 'd10:created'
\n\t\t\t || $start === 'd13:creatio'
\n\t\t\t || $start === 'd13:announc'
\n\t\t\t || $start === 'd12:_info_l'
\n\t\t\t || substr($start, 0, 10) === 'd7:comment' // @see https://github.com/adriengibrat/torrent-rw/issues/32
\n\t\t\t || substr($start, 0, 7) === 'd4:info'
\n\t\t\t || substr($start, 0, 3) === 'd9:'; // @see https://github.com/adriengibrat/torrent-rw/pull/17
\n\t}
\n
\n\t/** Helper to get (distant) file content
\n\t * @param string file location
\n\t * @param float http timeout (optional, default to self::timeout 30s)
\n\t * @param integer starting offset (optional, default to null)
\n\t * @param integer content length (optional, default to null)
\n\t * @return string|boolean file content or false if error
\n\t */
\n\tstatic public function file_get_contents ( $file, $timeout = self::timeout, $offset = null, $length = null ) {
\n\t\tif ( is_file( $file ) || ini_get( 'allow_url_fopen' ) ) {
\n\t\t\t$context = ! is_file( $file ) && $timeout ? 
\n\t\t\t\tstream_context_create( array( 'http' => array( 'timeout' => $timeout ) ) ) : 
\n\t\t\t\tnull;
\n\t\t\treturn ! is_null( $offset ) ? $length ?
\n\t\t\t\t@file_get_contents( $file, false, $context, $offset, $length ) : 
\n\t\t\t\t@file_get_contents( $file, false, $context, $offset ) : 
\n\t\t\t\t@file_get_contents( $file, false, $context );
\n\t\t} elseif ( ! function_exists( 'curl_init' ) )
\n\t\t\treturn self::set_error( new Exception( 'Install CURL or enable "allow_url_fopen"' ) );
\n\t\t$handle = curl_init( $file );
\n\t\tif ( $timeout )
\n\t\t\tcurl_setopt( $handle, CURLOPT_TIMEOUT, $timeout );
\n\t\tif ( $offset || $length )
\n\t\t\tcurl_setopt( $handle, CURLOPT_RANGE, $offset . '-' . ( $length ? $offset + $length -1 : null ) );
\n\t\tcurl_setopt( $handle, CURLOPT_RETURNTRANSFER, 1 );
\n\t\t$content = curl_exec( $handle );
\n\t\t$size = curl_getinfo( $handle, CURLINFO_CONTENT_LENGTH_DOWNLOAD );
\n\t\tcurl_close( $handle );
\n\t\treturn ( $offset && $size == -1 ) || ( $length && $length != $size ) ? $length ?
\n\t\t\tsubstr( $content, $offset, $length) :
\n\t\t\tsubstr( $content, $offset) :
\n\t\t\t$content;
\n\t}
\n
\n\t/** Flatten announces list
\n\t * @param array announces list
\n\t * @return array flattened announces list
\n\t */
\n\tstatic public function untier( $announces ) {
\n\t\t$list = array();
\n\t\tforeach ( (array) $announces as $tier ) {
\n\t\t\tis_array( $tier ) ? 
\n\t\t\t\t$list = array_merge( $list, self::untier( $tier ) ) :
\n\t\t\t\tarray_push( $list, $tier );
\n\t\t}
\n\t\treturn $list;
\n\t}
\n
\n}
\n