diff --git a/app/Client/transmission/TransmissionRPCException.php b/app/Client/transmission/TransmissionRPCException.php new file mode 100644 index 0000000..770e03a --- /dev/null +++ b/app/Client/transmission/TransmissionRPCException.php @@ -0,0 +1,48 @@ +=')) { + parent::__construct($message, $code, $previous); + } else { + parent::__construct($message, $code); + } + } +} diff --git a/app/Client/transmission/transmission.php b/app/Client/transmission/transmission.php new file mode 100644 index 0000000..71bf719 --- /dev/null +++ b/app/Client/transmission/transmission.php @@ -0,0 +1,759 @@ +, + * Bryce Chidester + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * PHP version specific information + * version_compare() (PHP 4 >= 4.1.0, PHP 5) + * ctype_digit() (PHP 4 >= 4.0.4, PHP 5) + * stream_context_create (PHP 4 >= 4.3.0, PHP 5) + * PHP Class support (PHP 5) (PHP 4 might work, untested) + */ + +namespace IYUU\Client\transmission; + +use IYUU\Client\AbstractClient; + +/** + * A friendly little version check... + */ +if (version_compare(PHP_VERSION, '5.2.10', '<')) { + die("The TransmissionRPC class requires PHP version 5.2.10 or above." . PHP_EOL); +} + +/** + * Transmission bittorrent client/daemon RPC communication class + * + * Usage example: + * + * $rpc = new TransmissionRPC($rpc_url); + * $result = $rpc->add_file( $url_or_path_to_torrent, $target_folder ); + * + * + */ +class transmission extends AbstractClient +{ + /** + * User agent used in all http communication + */ + const HTTP_UA = 'TransmissionRPC for PHP/0.3'; + + /** + * The URL to the bittorent client you want to communicate with + * the port (default: 9091) can be set in you Transmission preferences + * @var string + */ + public $url = ''; + + /** + * If your Transmission RPC requires authentication, supply username here + * @var string + */ + public $username = ''; + + /** + * If your Transmission RPC requires authentication, supply password here + * @var string + */ + public $password = ''; + + /** + * Print debugging information, default is off + * @var bool + */ + public $debug = false; + + /** + * Transmission RPC version + * @var int + */ + protected $rpc_version = 0; + + /** + * Transmission uses a session id to prevent CSRF attacks + * @var string + */ + protected $session_id = ''; + + /** + * Default values for stream context + * @var array + */ + private $default_context_opts = array( + 'http' => array( + 'user_agent' => self::HTTP_UA, + 'timeout' => '60', // Don't want to be too slow + 'ignore_errors' => true, // Leave the error parsing/handling to the code + ), + "ssl"=>array( + "verify_peer"=>false, + "verify_peer_name"=>false, + ) + ); + + /** + * Constants for torrent status + */ + const TR_STATUS_STOPPED = 0; + const TR_STATUS_CHECK_WAIT = 1; + const TR_STATUS_CHECK = 2; + const TR_STATUS_DOWNLOAD_WAIT = 3; + const TR_STATUS_DOWNLOAD = 4; + const TR_STATUS_SEED_WAIT = 5; + const TR_STATUS_SEED = 6; + + const RPC_LT_14_TR_STATUS_CHECK_WAIT = 1; + const RPC_LT_14_TR_STATUS_CHECK = 2; + const RPC_LT_14_TR_STATUS_DOWNLOAD = 4; + const RPC_LT_14_TR_STATUS_SEED = 8; + const RPC_LT_14_TR_STATUS_STOPPED = 16; + + /** + * Takes the connection parameters + * + * @param string $url + * @param string $username + * @param string $password + */ + public function __construct($url = 'http://127.0.0.1:9091/transmission/rpc', $username = null, $password = null) + { + $this->url = $url; + $this->username = $username; + $this->password = $password; + } + + /** + * Start one or more torrents + * + * @param int|array ids A list of transmission torrent ids + * @return mixed + * @throws TransmissionRPCException + */ + public function start($ids) + { + if (!is_array($ids)) { + $ids = array($ids); + } + $request = array("ids" => $ids); + return $this->request("torrent-start", $request); + } + + /** + * Stop one or more torrents + * + * @param int|array ids A list of transmission torrent ids + * @return mixed + * @throws TransmissionRPCException + */ + public function stop($ids) + { + if (!is_array($ids)) { + $ids = array($ids); + } + $request = array("ids" => $ids); + return $this->request("torrent-stop", $request); + } + + /** + * Reannounce one or more torrents + * + * @param int|array ids A list of transmission torrent ids + * @return mixed + * @throws TransmissionRPCException + */ + public function reannounce($ids) + { + if (!is_array($ids)) { + $ids = array($ids); + } + $request = array("ids" => $ids); + return $this->request("torrent-reannounce", $request); + } + + /** + * Verify one or more torrents + * + * @param int|array ids A list of transmission torrent ids + * @return mixed + * @throws TransmissionRPCException + */ + public function verify($ids) + { + if (!is_array($ids)) { + $ids = array($ids); + } + $request = array("ids" => $ids); + return $this->request("torrent-verify", $request); + } + + /** + * Get information on torrents in transmission, if the ids parameter is + * empty all torrents will be returned. The fields array can be used to return certain + * fields. Default fields are: "id", "name", "status", "doneDate", "haveValid", "totalSize". + * See https://github.com/transmission/transmission/blob/2.9x/extras/rpc-spec.txt for available fields + * + * @param array fields An array of return fields + * @param int|array ids A list of transmission torrent ids + * + * Request: + * { + * "arguments": { + * "fields": [ "id", "name", "totalSize" ], + * "ids": [ 7, 10 ] + * }, + * "method": "torrent-get", + * "tag": 39693 + * } + * + * Response: + * { + * "arguments": { + * "torrents": [ + * { + * "id": 10, + * "name": "Fedora x86_64 DVD", + * "totalSize": 34983493932, + * }, + * { + * "id": 7, + * "name": "Ubuntu x86_64 DVD", + * "totalSize", 9923890123, + * } + * ] + * }, + * "result": "success", + * "tag": 39693 + * } + * @return mixed + * @throws TransmissionRPCException + */ + public function get($ids = array(), $fields = array()) + { + if (!is_array($ids)) { + $ids = array($ids); + } // Convert $ids to an array if only a single id was passed + if (count($fields) == 0) { + $fields = array("id", "name", "status", "doneDate", "haveValid", "totalSize"); + } // Defaults + $request = array( + "fields" => $fields, + "ids" => $ids + ); + return $this->request("torrent-get", $request); + } + + /** + * Set properties on one or more torrents, available fields are: + * "bandwidthPriority" | number this torrent's bandwidth tr_priority_t + * "downloadLimit" | number maximum download speed (in K/s) + * "downloadLimited" | boolean true if "downloadLimit" is honored + * "files-wanted" | array indices of file(s) to download + * "files-unwanted" | array indices of file(s) to not download + * "honorsSessionLimits" | boolean true if session upload limits are honored + * "ids" | array torrent list, as described in 3.1 + * "location" | string new location of the torrent's content + * "peer-limit" | number maximum number of peers + * "priority-high" | array indices of high-priority file(s) + * "priority-low" | array indices of low-priority file(s) + * "priority-normal" | array indices of normal-priority file(s) + * "seedRatioLimit" | double session seeding ratio + * "seedRatioMode" | number which ratio to use. See tr_ratiolimit + * "uploadLimit" | number maximum upload speed (in K/s) + * "uploadLimited" | boolean true if "uploadLimit" is honored + * See https://github.com/transmission/transmission/blob/2.9x/extras/rpc-spec.txt for more information + * + * @param array arguments An associative array of arguments to set + * @param int|array ids A list of transmission torrent ids + * @return mixed + * @throws TransmissionRPCException + */ + public function set($ids = array(), $arguments = array()) + { + // See https://github.com/transmission/transmission/blob/2.9x/extras/rpc-spec.txt for available fields + if (!is_array($ids)) { + $ids = array($ids); + } // Convert $ids to an array if only a single id was passed + if (!isset($arguments['ids'])) { + $arguments['ids'] = $ids; + } // Any $ids given in $arguments overrides the method parameter + return $this->request("torrent-set", $arguments); + } + + /** + * Add a new torrent + * + * Available extra options: + * key | value type & description + * ---------------------+------------------------------------------------- + * "download-dir" | string path to download the torrent to + * "filename" | string filename or URL of the .torrent file + * "metainfo" | string base64-encoded .torrent content + * "paused" | boolean if true, don't start the torrent + * "peer-limit" | number maximum number of peers + * "bandwidthPriority" | number torrent's bandwidth tr_priority_t + * "files-wanted" | array indices of file(s) to download + * "files-unwanted" | array indices of file(s) to not download + * "priority-high" | array indices of high-priority file(s) + * "priority-low" | array indices of low-priority file(s) + * "priority-normal" | array indices of normal-priority file(s) + * + * Either "filename" OR "metainfo" MUST be included. + * All other arguments are optional. + * + * @param string $torrent_location The URL or path to the torrent file + * @param string $save_path Folder to save torrent in + * @param array $extra_options Optional extra torrent options + * @return mixed + * @throws TransmissionRPCException + */ + public function add_file($torrent_location, $save_path = '', $extra_options = array()) + { + if (!empty($save_path)) { + $extra_options['download-dir'] = $save_path; + } + $extra_options['filename'] = $torrent_location; + + return $this->request("torrent-add", $extra_options); + } + + /* Add a new torrent using a file path or a URL (For backwards compatibility) + * @param torrent_location The URL or path to the torrent file + * @param save_path Folder to save torrent in + * @param extra options Optional extra torrent options + */ + public function add($torrent_location, $save_path = '', $extra_options = array()) + { + return $this->add_file($torrent_location, $save_path, $extra_options); + } + + /** + * Add a torrent using the raw torrent data + * + * @param string $torrent_metainfo The raw, unencoded contents (metainfo) of a torrent + * @param string $save_path Folder to save torrent in + * @param array $extra_options Optional extra torrent options + * @return mixed + * @throws TransmissionRPCException + */ + public function add_metainfo($torrent_metainfo, $save_path = '', $extra_options = array()) + { + if (!empty($save_path)) { + $extra_options['download-dir'] = $save_path; + } + $extra_options['metainfo'] = base64_encode($torrent_metainfo); + + return $this->request("torrent-add", $extra_options); + } + + /** + * Remove torrent from transmission + * + * @param bool delete_local_data Also remove local data? + * @param int|array ids A list of transmission torrent ids + * @return mixed + * @throws TransmissionRPCException + */ + public function delete($ids, $delete_local_data = false) + { + if (!is_array($ids)) { + $ids = array($ids); + } // Convert $ids to an array if only a single id was passed + $request = array( + "ids" => $ids, + "delete-local-data" => $delete_local_data + ); + return $this->request("torrent-remove", $request); + } + + /** + * Move local storage location + * + * @param int|array ids A list of transmission torrent ids + * @param string target_location The new storage location + * @param string move_existing_data Move existing data or scan new location for available data + * @return mixed + * @throws TransmissionRPCException + */ + public function move($ids, $target_location, $move_existing_data = true) + { + if (!is_array($ids)) { + $ids = array($ids); + } // Convert $ids to an array if only a single id was passed + $request = array( + "ids" => $ids, + "location" => $target_location, + "move" => $move_existing_data + ); + return $this->request("torrent-set-location", $request); + } + + /** + * 3.7. Renaming a Torrent's Path + * + * Method name: "torrent-rename-path" + * + * For more information on the use of this function, see the transmission.h + * documentation of tr_torrentRenamePath(). In particular, note that if this + * call succeeds you'll want to update the torrent's "files" and "name" field + * with torrent-get. + * + * Request arguments: + * + * string | value type & description + * ---------------------------------+------------------------------------------------- + * "ids" | array the torrent torrent list, as described in 3.1 + * | (must only be 1 torrent) + * "path" | string the path to the file or folder that will be renamed + * "name" | string the file or folder's new name + * Response arguments: "path", "name", and "id", holding the torrent ID integer + * + * @param int|array ids A 1-element list of transmission torrent ids + * @param string path The path to the file or folder that will be renamed + * @param string name The file or folder's new name + * @return mixed + * @throws TransmissionRPCException + */ + public function rename($ids, $path, $name) + { + if (!is_array($ids)) { + $ids = array($ids); + } // Convert $id to an array if only a single id was passed + if (count($ids) !== 1) { + throw new TransmissionRPCException('A single id is accepted', TransmissionRPCException::E_INVALIDARG); + } + + $request = array( + "ids" => $ids, + "path" => $path, + "name" => $name + ); + return $this->request("torrent-rename-path", $request); + } + + + /** + * Retrieve session statistics + * + * @returns array of statistics + */ + public function sstats() + { + return $this->request("session-stats", array()); + } + + /** + * Retrieve all session variables + * + * @returns array of session information + */ + public function sget() + { + return $this->request("session-get", array()); + } + + /** + * Set session variable(s) + * + * @param array of session variables to set + * @return mixed + * @throws TransmissionRPCException + */ + public function sset($arguments) + { + return $this->request("session-set", $arguments); + } + + /** + * Return the interpretation of the torrent status + * + * @param int The integer "torrent status" + * @returns string The translated meaning + * @return string + */ + public function getStatusString($intstatus) + { + if ($this->rpc_version < 14) { + if ($intstatus == self::RPC_LT_14_TR_STATUS_CHECK_WAIT) { + return "Waiting to verify local files"; + } + if ($intstatus == self::RPC_LT_14_TR_STATUS_CHECK) { + return "Verifying local files"; + } + if ($intstatus == self::RPC_LT_14_TR_STATUS_DOWNLOAD) { + return "Downloading"; + } + if ($intstatus == self::RPC_LT_14_TR_STATUS_SEED) { + return "Seeding"; + } + if ($intstatus == self::RPC_LT_14_TR_STATUS_STOPPED) { + return "Stopped"; + } + } else { + if ($intstatus == self::TR_STATUS_CHECK_WAIT) { + return "Waiting to verify local files"; + } + if ($intstatus == self::TR_STATUS_CHECK) { + return "Verifying local files"; + } + if ($intstatus == self::TR_STATUS_DOWNLOAD) { + return "Downloading"; + } + if ($intstatus == self::TR_STATUS_SEED) { + return "Seeding"; + } + if ($intstatus == self::TR_STATUS_STOPPED) { + return "Stopped"; + } + if ($intstatus == self::TR_STATUS_SEED_WAIT) { + return "Queued for seeding"; + } + if ($intstatus == self::TR_STATUS_DOWNLOAD_WAIT) { + return "Queued for download"; + } + } + return "Unknown"; + } + + + + /** + * Here be dragons (Internal methods) + */ + + + /** + * Clean up the request array. Removes any empty fields from the request + * + * @param array array The request associative array to clean + * @returns array The cleaned array + * @return array|null + */ + protected function cleanRequestData($array) + { + if (!is_array($array) || count($array) == 0) { + return null; + } // Nothing to clean + setlocale(LC_NUMERIC, 'en_US.utf8'); // Override the locale - if the system locale is wrong, then 12.34 will encode as 12,34 which is invalid JSON + foreach ($array as $index => $value) { + if (is_object($value)) { + $array[$index] = $value->toArray(); + } // Convert objects to arrays so they can be JSON encoded + if (is_array($value)) { + $array[$index] = $this->cleanRequestData($value); + } // Recursion + if (empty($value) && $value !== 0) { // Remove empty members + unset($array[$index]); + continue; // Skip the rest of the tests - they may re-add the element. + } + if (is_numeric($value)) { + $array[$index] = $value + 0; + } // Force type-casting for proper JSON encoding (+0 is a cheap way to maintain int/float/etc) + if (is_bool($value)) { + $array[$index] = ($value ? 1 : 0); + } // Store boolean values as 0 or 1 + if (is_string($value)) { + $type = mb_detect_encoding($value, "auto"); + if ($type !== 'UTF-8') { + $array[$index] = mb_convert_encoding($value, "UTF-8"); + //utf8_encode( $value ); // Make sure all data is UTF-8 encoded for Transmission + } + } + } + return $array; + } + + /** + * 执行 rpc 请求 + * + * @param string $method 请求类型/方法, 详见 $this->allowMethods + * @param array $arguments 附加参数, 可选 + * @return array + * @throws TransmissionRPCException + */ + protected function request($method, $arguments = array()) + { + // Check the parameters + if (!is_scalar($method)) { + throw new TransmissionRPCException('Method name has no scalar value', TransmissionRPCException::E_INVALIDARG); + } + if (!is_array($arguments)) { + throw new TransmissionRPCException('Arguments must be given as array', TransmissionRPCException::E_INVALIDARG); + } + + $arguments = $this->cleanRequestData($arguments); // Sanitize input + + // Grab the X-Transmission-Session-Id if we don't have it already + if (!$this->session_id) { + if (!$this->GetSessionID()) { + throw new TransmissionRPCException('Unable to acquire X-Transmission-Session-Id', TransmissionRPCException::E_SESSIONID); + } + } + + $data = array( + 'method' => $method, + 'arguments' => $arguments + ); + + $header = array( + 'Content-Type: application/json', + 'Authorization: Basic ' . base64_encode(sprintf("%s:%s", $this->username, $this->password)), + 'X-Transmission-Session-Id: ' . $this->session_id + ); + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $this->url); + curl_setopt($ch, CURLOPT_HTTPHEADER, $header); + curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); + curl_setopt($ch, CURLOPT_USERPWD, $this->username . ':' . $this->password); + curl_setopt($ch, CURLOPT_HEADER, false); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 60); + curl_setopt($ch, CURLOPT_TIMEOUT, 600); + $content = curl_exec($ch); + curl_close($ch); + + if (!$content) { + $content = array('result' => 'failed'); + } + return json_decode($content, true); + } + + /** + * Performs an empty GET on the Transmission RPC to get the X-Transmission-Session-Id + * and store it in $this->session_id + * + * @return string + * @throws TransmissionRPCException + */ + public function GetSessionID() + { + if (!$this->url) { + throw new TransmissionRPCException("Class must be initialized before GetSessionID() can be called.", TransmissionRPCException::E_INVALIDARG); + } + + // Setup the context + $contextopts = $this->default_context_opts; // Start with the defaults + + // Make sure it's blank/empty (reset) + $this->session_id = null; + + // Setup authentication (if provided) + if ($this->username && $this->password) { + $contextopts['http']['header'] = sprintf("Authorization: Basic %s\r\n", base64_encode($this->username . ':' . $this->password)); + } + + if ($this->debug) { + echo "TRANSMISSIONRPC_DEBUG:: GetSessionID():: Stream context created with options:" . + PHP_EOL . print_r($contextopts, true); + } + + $context = stream_context_create($contextopts); // Create the context for this request + if (!$fp = @fopen($this->url, 'r', false, $context)) { // Open a filepointer to the data, and use fgets to get the result + throw new TransmissionRPCException('Unable to connect to ' . $this->url, TransmissionRPCException::E_CONNECTION); + } + + // Check the response (headers etc) + $stream_meta = stream_get_meta_data($fp); + fclose($fp); + if ($this->debug) { + echo "TRANSMISSIONRPC_DEBUG:: GetSessionID():: Stream meta info: " . + PHP_EOL . print_r($stream_meta, true); + } + if ($stream_meta['timed_out']) { + throw new TransmissionRPCException("Timed out connecting to {$this->url}", TransmissionRPCException::E_CONNECTION); + } + if (substr($stream_meta['wrapper_data'][0], 9, 3) == "401") { + throw new TransmissionRPCException("Invalid username/password.", TransmissionRPCException::E_AUTHENTICATION); + } elseif (substr($stream_meta['wrapper_data'][0], 9, 3) == "409") { // This is what we're hoping to find + // Loop through the returned headers and extract the X-Transmission-Session-Id + foreach ($stream_meta['wrapper_data'] as $header) { + if (strpos($header, 'X-Transmission-Session-Id: ') === 0) { + if ($this->debug) { + echo "TRANSMISSIONRPC_DEBUG:: GetSessionID():: Session-Id header: " . + PHP_EOL . print_r($header, true); + } + $this->session_id = trim(substr($header, 27)); + break; + } + } + if (!$this->session_id) { // Didn't find a session_id + throw new TransmissionRPCException("Unable to retrieve X-Transmission-Session-Id", TransmissionRPCException::E_SESSIONID); + } + } else { + throw new TransmissionRPCException("Unexpected response from Transmission RPC: " . $stream_meta['wrapper_data'][0]); + } + return $this->session_id; + } + + /** + * 抽象方法,子类实现 + */ + public function status() + { + $rs = $this->sstats(); + return isset($rs['result']) ? $rs['result'] : 'error'; + } + + /** + * 抽象方法,子类实现 + */ + public function getList(&$move = array()) + { + $ids = array(); + $fields = array( "id", "status", "name", "hashString", "downloadDir", "torrentFile" ); + $res = $this->get($ids, $fields); + if (isset($res['result']) && $res['result'] == 'success') { + // 成功 + } else { + // 失败 + echo "获取种子列表失败,可能transmission暂时无响应,请稍后重试!".PHP_EOL; + return array(); + } + if (empty($res['arguments']['torrents'])) { + echo "未获取到正常做种数据,请多保种,然后重试!".PHP_EOL; + return array(); + } + $res = $res['arguments']['torrents']; + // 过滤,只保留正常做种 + $res = array_filter($res, function ($v) { + return isset($v['status']) && $v['status']===6; + }, ARRAY_FILTER_USE_BOTH); + + if (empty($res)) { + echo "未获取到正常做种数据,请多保种,然后重试!".PHP_EOL; + return array(); + } + // 提取数组:hashString + $info_hash = array_column($res, 'hashString'); + // 升序排序 + sort($info_hash); + $json = json_encode($info_hash, JSON_UNESCAPED_UNICODE); + // 去重 应该从文件读入,防止重复提交 + $sha1 = sha1($json); + // 组装返回数据 + $hashArray['hash'] = $json; + $hashArray['sha1'] = $sha1; + // 变换数组:hashString为键 + $hashArray['hashString'] = array_column($res, "downloadDir", 'hashString'); + $move = array_column($res, null, 'hashString'); + return $hashArray; + } +}