├── .editorconfig ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── Client.php ├── Exception ├── ErrorException.php ├── ExceptionInterface.php ├── InvalidArgumentException.php ├── NetworkException.php ├── RuntimeException.php └── TransmissionException.php ├── Formatter.php ├── HttpClient ├── Builder.php ├── Message │ ├── ParamBuilder.php │ └── ResponseMediator.php ├── Plugin │ ├── AuthSession.php │ ├── ExceptionThrower.php │ └── History.php └── RequestBuilder.php ├── Laravel ├── Facade.php ├── ServiceProvider.php └── config │ └── transmission.php └── Models ├── AbstractModel.php └── Torrent.php /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | indent_size = 4 9 | indent_style = space 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes will be documented in this file. 4 | 5 | ## 1.4 - 2018-12-02 6 | 7 | ### Changed 8 | 9 | - Upgrade Dependency Packages. 10 | - Typehint and cast. 11 | - Optimize Code. 12 | 13 | ## 1.3.1 - 2018-08-16 14 | 15 | ### Changed 16 | 17 | - Param Builder: IDs can be of string value. So no longer typecasting to int value only. 18 | 19 | ## 1.3.0 - 2018-08-14 20 | 21 | ### Added 22 | 23 | - Add `duplicate` key to `add()` method to indicate the responded transfer info is of a duplicate torrent. 24 | - Torrent Model: `isFolder()` and `isMagnet()` methods. 25 | - Param Builder: Add more typecasting to param builder. 26 | 27 | ### Changed 28 | 29 | - Catch Network Exception and rethrow with our own network exception. 30 | 31 | ## 1.2.0 - 2018-08-01 32 | 33 | ### Added 34 | - Client: `getAll()`, `startAll()`, `stopAll()`, `addFile()`, `addUrl()` methods. 35 | - Torrent Model: `needsMetaData()`, `getDownloadSpeed()`, `getUploadSpeed()`, `getRecheckProgress()` and `getMetadataPercentComplete()`. 36 | - Model: Optional casting toggle argument to the `get()` method. 37 | - Param Builder: Add custom array wrapper. 38 | - Speed and TrunicateNumber Methods to Formatter and Casting. 39 | - More casting types: timestamp, memory, datarate. 40 | 41 | ### Changed 42 | - Rename Helper to Formatter. 43 | - Rename `bytes` to `size` casting type. 44 | - Refactor `add()` method. No longer fetches duplicate torrent's additional data. 45 | - `seedRatioLimit()` in Torrent Model now supports integer and `Client` value for global seed ratio limit. 46 | - `get()` method now returns collection to be consistent. 47 | - Param Builder: Encodes strings in UTF-8 format as required by Transmission. 48 | - Param Builder: `ids` argument is now fully compatible as per specs to support `recently-active`. 49 | 50 | ## 1.1.0 - 2018-07-29 51 | 52 | - Bugfixes and Optimization. 53 | - Add Helper Class with Casting and Formatting Methods. 54 | - Add Torrent Model. 55 | - Add `seedRatioLimit()` method. 56 | - Refactor Network Exception. 57 | - Improve typehinting. 58 | - Improve Exception Thrower Plugin. 59 | 60 | ## 1.0.0 - 2018-07-28 61 | 62 | - Initial Release -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at `syed at lukonet.com`. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | Please read and understand the contribution guide before creating an issue or pull request. 6 | 7 | ## Etiquette 8 | 9 | This project is open source, and as such, the maintainers give their free time to build and maintain the source code 10 | held within. They make the code freely available in the hope that it will be of use to other developers. It would be 11 | extremely unfair for them to suffer abuse or anger for their hard work. 12 | 13 | Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the 14 | world that developers are civilized and selfless people. 15 | 16 | It's the duty of the maintainer to ensure that all submissions to the project are of sufficient 17 | quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. 18 | 19 | ## Viability 20 | 21 | When requesting or submitting new features, first consider whether it might be useful to others. Open 22 | source projects are used by many developers, who may have entirely different needs to your own. Think about 23 | whether or not your feature is likely to be used by other users of the project. 24 | 25 | ## Procedure 26 | 27 | Before filing an issue: 28 | 29 | - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. 30 | - Check to make sure your feature suggestion isn't already present within the project. 31 | - Check the pull requests tab to ensure that the bug doesn't have a fix in progress. 32 | - Check the pull requests tab to ensure that the feature isn't already in progress. 33 | 34 | Before submitting a pull request: 35 | 36 | - Check the codebase to ensure that your feature doesn't already exist. 37 | - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. 38 | 39 | ## Requirements 40 | 41 | If the project maintainer has any additional requirements, you will find them listed here. 42 | 43 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). 44 | 45 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 46 | 47 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 48 | 49 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 50 | 51 | - **Create feature branches** - Don't ask us to pull from your master branch. 52 | 53 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 54 | 55 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 56 | 57 | 58 | ## Running Tests 59 | 60 | ``` bash 61 | $ composer test 62 | ``` 63 | 64 | **Happy coding**! 65 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Syed Irfaq R. 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP Transmission-RPC API SDK 2 | 3 | [![Join PHP Chat][ico-phpchat]][link-phpchat] 4 | [![Chat on Telegram][ico-telegram]][link-telegram] 5 | [![Laravel Package][ico-laravel]][link-repo] 6 | [![Latest Version on Packagist][ico-version]][link-packagist] 7 | [![Software License][ico-license]](LICENSE.md) 8 | [![Quality Score][ico-code-quality]][link-code-quality] 9 | [![Total Downloads][ico-downloads]][link-downloads] 10 | 11 | > A Transmission-RPC API SDK for PHP with Laravel Support. 12 | 13 | ## Install 14 | 15 | Via Composer 16 | 17 | ``` bash 18 | $ composer require irazasyed/php-transmission-sdk php-http/guzzle6-adapter 19 | ``` 20 | 21 | > **Note:** You can use HTTP Client of your choice, for the list of adapters please check [HTTPlug](http://httplug.io/). 22 | 23 | ### Laravel 24 | 25 | > This package supports the [package discovery](https://laravel.com/docs/5.5/packages#package-discovery) functionality provided in Laravel >= 5.5, so you don't have to manually register the service provider or facade. 26 | 27 | ### Configuration - (Optional) 28 | 29 | Copy the config file into your project 30 | 31 | ``` bash 32 | php artisan vendor:publish --provider="Transmission\Laravel\ServiceProvider" 33 | ``` 34 | 35 | ## Usage 36 | 37 | ``` php 38 | $transmission = new Transmission\Client($hostname, $port, $username, $password, $httpClientBuilder = null); 39 | $transmission->get(); // Get All Torrents. 40 | 41 | // Laravel 42 | $transfers = Transmission::get('recently-active'); // 43 | ``` 44 | 45 | > The SDK supports all the methods listed in specs. For more details, check out [transmission-rpc specs](https://git.io/transmission-rpc-specs). 46 | 47 | [![Transmission-RPC API SDK Usage][sdk-usage]][link-repo] 48 | 49 | ## Change log 50 | 51 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 52 | 53 | ## Testing 54 | 55 | ``` bash 56 | $ composer test 57 | ``` 58 | 59 | ## Contributing 60 | 61 | Please see [CONTRIBUTING](CONTRIBUTING.md) and [CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) for details. 62 | 63 | ## Security 64 | 65 | If you discover any security related issues, please email gh@lukonet.com instead of using the issue tracker. 66 | 67 | ## Credits 68 | 69 | - [Syed][link-author] 70 | - [All Contributors][link-contributors] 71 | 72 | ## License 73 | 74 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 75 | 76 | [ico-phpchat]: https://img.shields.io/badge/Slack-PHP%20Chat-5c6aaa.svg?style=flat-square&logo=slack&labelColor=4A154B 77 | [ico-telegram]: https://img.shields.io/badge/@PHPChatCo-2CA5E0.svg?style=flat-square&logo=telegram&label=Telegram 78 | [ico-laravel]: https://img.shields.io/badge/Laravel-5~8-FF2D20.svg?style=flat-square&logo=laravel&labelColor=black&logoColor=white 79 | [ico-version]: https://img.shields.io/packagist/v/irazasyed/php-transmission-sdk.svg?style=flat-square 80 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square 81 | [ico-travis]: https://img.shields.io/travis/irazasyed/php-transmission-sdk/master.svg?style=flat-square 82 | [ico-scrutinizer]: https://img.shields.io/scrutinizer/coverage/g/irazasyed/php-transmission-sdk.svg?style=flat-square 83 | [ico-code-quality]: https://img.shields.io/scrutinizer/g/irazasyed/php-transmission-sdk.svg?style=flat-square 84 | [ico-downloads]: https://img.shields.io/packagist/dt/irazasyed/php-transmission-sdk.svg?style=flat-square 85 | 86 | [sdk-usage]: https://user-images.githubusercontent.com/1915268/43494955-0c47765e-9553-11e8-851e-14139d8bcd0d.png 87 | 88 | [link-phpchat]: https://phpchat.co/?ref=php-transmission-sdk 89 | [link-telegram]: https://t.me/PHPChatCo 90 | [link-repo]: https://github.com/irazasyed/php-transmission-sdk 91 | [link-packagist]: https://packagist.org/packages/irazasyed/php-transmission-sdk 92 | [link-travis]: https://travis-ci.org/irazasyed/php-transmission-sdk 93 | [link-scrutinizer]: https://scrutinizer-ci.com/g/irazasyed/php-transmission-sdk/code-structure 94 | [link-code-quality]: https://scrutinizer-ci.com/g/irazasyed/php-transmission-sdk 95 | [link-downloads]: https://packagist.org/packages/irazasyed/php-transmission-sdk 96 | [link-author]: https://github.com/irazasyed 97 | [link-contributors]: ../../contributors 98 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "irazasyed/php-transmission-sdk", 3 | "type": "library", 4 | "description": "A Transmission-RPC API SDK for PHP with Laravel Support", 5 | "keywords": [ 6 | "transmission", 7 | "transmission api sdk", 8 | "transmission-rpc", 9 | "php-transmission-sdk", 10 | "laravel transmission", 11 | "php transmission client" 12 | ], 13 | "homepage": "https://github.com/irazasyed/php-transmission-sdk", 14 | "license": "MIT", 15 | "authors": [ 16 | { 17 | "name": "Syed I.R.", 18 | "email": "gh@lukonet.com", 19 | "homepage": "https://github.com/irazasyed", 20 | "role": "Developer" 21 | } 22 | ], 23 | "require": { 24 | "php" : "^7.2 || ^8.0", 25 | "nesbot/carbon": "^2.5", 26 | "illuminate/support": "^5.5 || ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0", 27 | "psr/http-client": "^1.0", 28 | "php-http/discovery": "^1.6", 29 | "php-http/client-common": "^2.0", 30 | "php-http/client-implementation": "^1.0" 31 | }, 32 | "require-dev": { 33 | "phpunit/phpunit" : "^8.0", 34 | "nyholm/psr7": "^1.1", 35 | "php-http/guzzle6-adapter": "^2.0", 36 | "php-http/mock-client": "^1.0" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "Transmission\\": "src" 41 | } 42 | }, 43 | "autoload-dev": { 44 | "psr-4": { 45 | "Transmission\\Tests\\": "tests" 46 | } 47 | }, 48 | "scripts": { 49 | "test": "vendor/bin/phpunit", 50 | "test-coverage": "phpunit --coverage-html coverage" 51 | }, 52 | "extra": { 53 | "branch-alias": { 54 | "dev-master": "2.0-dev" 55 | } 56 | }, 57 | "config": { 58 | "sort-packages": true 59 | }, 60 | "extra": { 61 | "laravel": { 62 | "providers": [ 63 | "Transmission\\Laravel\\ServiceProvider" 64 | ], 65 | "aliases": { 66 | "Transmission": "Transmission\\Laravel\\Facade" 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | host = $host ?? '127.0.0.1'; 67 | $this->port = $port ?? 9091; 68 | 69 | $this->responseHistory = new History(); 70 | $this->httpClientBuilder = $httpClientBuilder ?? new Builder(); 71 | $this->httpClientBuilder->addPlugin(new ExceptionThrower()); 72 | $this->httpClientBuilder->addPlugin(new HistoryPlugin($this->responseHistory)); 73 | $this->httpClientBuilder->addPlugin(new HeaderDefaultsPlugin([ 74 | 'User-Agent' => $this->defaultUserAgent(), 75 | ])); 76 | 77 | if (filled($username)) { 78 | $this->authenticate($username, $password); 79 | } 80 | } 81 | 82 | /** 83 | * Create a Transmission\Client. 84 | * 85 | * @param null|string $host 86 | * @param null|int $port 87 | * @param null|string $username 88 | * @param null|string $password 89 | * 90 | * @return Client 91 | */ 92 | public static function create( 93 | string $host = null, 94 | int $port = null, 95 | string $username = null, 96 | string $password = null 97 | ): self { 98 | return new static($host, $port, $username, $password); 99 | } 100 | 101 | /** 102 | * Create a Transmission\Client using an HttpClient. 103 | * 104 | * @param ClientInterface $httpClient 105 | * @param null|string $host 106 | * @param null|int $port 107 | * @param null|string $username 108 | * @param null|string $password 109 | * 110 | * @return Client 111 | */ 112 | public static function createWithHttpClient( 113 | ClientInterface $httpClient, 114 | string $host = null, 115 | int $port = null, 116 | string $username = null, 117 | string $password = null 118 | ): self { 119 | return new static($host, $port, $username, $password, new Builder($httpClient)); 120 | } 121 | 122 | /** 123 | * Determine if TLS is enabled. 124 | * 125 | * @return bool 126 | */ 127 | public function isTLSEnabled(): bool 128 | { 129 | return $this->enableTLS; 130 | } 131 | 132 | /** 133 | * Enable TLS. 134 | * 135 | * @return $this 136 | */ 137 | public function enableTLS(): self 138 | { 139 | $this->enableTLS = true; 140 | 141 | return $this; 142 | } 143 | 144 | /** 145 | * Get Client Instance. 146 | * 147 | * @return Client 148 | */ 149 | public function instance(): self 150 | { 151 | return $this; 152 | } 153 | 154 | /** 155 | * Authenticate the user for all next requests. 156 | * 157 | * @param string $username 158 | * @param null|string $password 159 | * 160 | * @return Client 161 | */ 162 | public function authenticate(string $username, string $password = ''): self 163 | { 164 | $authentication = new BasicAuth($username, $password); 165 | 166 | $this->httpClientBuilder->removePlugin(AuthenticationPlugin::class); 167 | $this->httpClientBuilder->addPlugin(new AuthenticationPlugin($authentication)); 168 | 169 | return $this; 170 | } 171 | 172 | /** 173 | * Set Session ID. 174 | * 175 | * @param string $sessionId 176 | * 177 | * @return Client 178 | */ 179 | public function setSessionId(string $sessionId): self 180 | { 181 | $this->httpClientBuilder->removePlugin(AuthSession::class); 182 | $this->httpClientBuilder->addPlugin(new AuthSession($sessionId)); 183 | 184 | return $this; 185 | } 186 | 187 | /** 188 | * Start All Torrents. 189 | * 190 | * @return bool 191 | * 192 | * @see start() 193 | */ 194 | public function startAll(): bool 195 | { 196 | return $this->start(); 197 | } 198 | 199 | /** 200 | * Start one or more torrents. 201 | * 202 | * @see https://git.io/transmission-rpc-specs Transfer Action Requests. 203 | * 204 | * @param mixed $ids One or more torrent ids, sha1 hash strings, or both OR "recently-active", for 205 | * recently-active torrents. All torrents are used if no value is given. 206 | * 207 | * @return bool 208 | */ 209 | public function start($ids = null): bool 210 | { 211 | $this->api('torrent-start', compact('ids')); 212 | 213 | return true; 214 | } 215 | 216 | /** 217 | * Start Now one or more torrents. 218 | * 219 | * @see https://git.io/transmission-rpc-specs Torrent Action Requests. 220 | * 221 | * @param mixed $ids One or more torrent ids, as described in 3.1 of specs. 222 | * 223 | * @return bool 224 | */ 225 | public function startNow($ids = null): bool 226 | { 227 | $this->api('torrent-start-now', compact('ids')); 228 | 229 | return true; 230 | } 231 | 232 | /** 233 | * Stop All Torrents. 234 | * 235 | * @return bool 236 | * 237 | * @see stop() 238 | */ 239 | public function stopAll(): bool 240 | { 241 | return $this->stop(); 242 | } 243 | 244 | /** 245 | * Stop one or more torrents. 246 | * 247 | * @see https://git.io/transmission-rpc-specs Torrent Action Requests. 248 | * 249 | * @param mixed $ids One or more torrent ids, as described in 3.1 of specs. 250 | * 251 | * @return bool 252 | */ 253 | public function stop($ids = null): bool 254 | { 255 | $this->api('torrent-stop', compact('ids')); 256 | 257 | return true; 258 | } 259 | 260 | /** 261 | * Verify one or more torrents. 262 | * 263 | * @see https://git.io/transmission-rpc-specs Torrent Action Requests. 264 | * 265 | * @param mixed $ids One or more torrent ids, as described in 3.1 of specs. 266 | * 267 | * @return bool 268 | */ 269 | public function verify($ids = null): bool 270 | { 271 | $this->api('torrent-verify', compact('ids')); 272 | 273 | return true; 274 | } 275 | 276 | /** 277 | * Reannounce one or more torrents. 278 | * 279 | * @see https://git.io/transmission-rpc-specs Torrent Action Requests. 280 | * 281 | * @param mixed $ids One or more torrent ids, as described in 3.1 of specs. 282 | * 283 | * @return bool 284 | */ 285 | public function reannounce($ids = null): bool 286 | { 287 | $this->api('torrent-reannounce', compact('ids')); 288 | 289 | return true; 290 | } 291 | 292 | /** 293 | * Set properties of one or more torrents. 294 | * 295 | * @see https://git.io/transmission-rpc-specs "torrent-set" for available arguments. 296 | * 297 | * @param mixed $ids One or more torrent ids, as described in 3.1 of specs. 298 | * @param array $arguments An associative array of arguments to set. 299 | * 300 | * @return bool 301 | */ 302 | public function set($ids, array $arguments): bool 303 | { 304 | $arguments['ids'] = $ids; 305 | $this->api('torrent-set', $arguments); 306 | 307 | return true; 308 | } 309 | 310 | /** 311 | * Get All Torrents. 312 | * 313 | * @param array|null $fields 314 | * 315 | * @return Collection 316 | */ 317 | public function getAll(array $fields = null): Collection 318 | { 319 | return $this->get(null, $fields); 320 | } 321 | 322 | /** 323 | * Get information on torrents, if the ids parameter is 324 | * null all torrents will be returned. 325 | * 326 | * @see https://git.io/transmission-rpc-specs "torrent-get" for available fields. 327 | * 328 | * @param mixed $ids One or more torrent ids, as described in 3.1 of specs. 329 | * @param array $fields An array of return fields, no value will fallback to default fields. 330 | * 331 | * @return Collection 332 | */ 333 | public function get($ids = null, array $fields = null): Collection 334 | { 335 | $fields = $fields ?? Torrent::$fields['default']; 336 | $data = $this->api('torrent-get', compact('ids', 'fields')); 337 | 338 | $torrentsInfo = data_get($data, 'arguments.torrents', 0); 339 | 340 | if (blank($torrentsInfo)) { 341 | return collect(); 342 | } 343 | 344 | return collect($torrentsInfo)->mapInto(Torrent::class); 345 | } 346 | 347 | /** 348 | * Add a Torrent File to the download queue. 349 | * 350 | * @param string $file Torrent File Content. 351 | * @param string|null $savepath Path to download the torrent to. 352 | * @param array $optionalArgs Other optional arguments. 353 | * 354 | * @return Collection 355 | */ 356 | public function addFile($file, string $savepath = null, array $optionalArgs = []): Collection 357 | { 358 | return $this->add($file, true, $savepath, $optionalArgs); 359 | } 360 | 361 | /** 362 | * Add a Torrent by URL to the download queue. 363 | * 364 | * @param string $url Magnet URI/URL of the torrent file. 365 | * @param string|null $savepath Path to download the torrent to. 366 | * @param array $optionalArgs Other optional arguments. 367 | * 368 | * @return Collection 369 | */ 370 | public function addUrl($url, string $savepath = null, array $optionalArgs = []): Collection 371 | { 372 | return $this->add($url, false, $savepath, $optionalArgs); 373 | } 374 | 375 | /** 376 | * Add a torrent to the download queue. 377 | * 378 | * @see https://git.io/transmission-rpc-specs "torrent-add" for available arguments. 379 | * 380 | * @param string $torrent Magnet URI/URL of the torrent file OR .torrent content. 381 | * @param bool $metainfo Is given torrent a metainfo? (default: false). 382 | * @param string $savepath Path to download the torrent to. 383 | * @param array $optionalArgs Other optional arguments. 384 | * 385 | * @return Collection 386 | */ 387 | public function add( 388 | string $torrent, 389 | bool $metainfo = false, 390 | string $savepath = null, 391 | array $optionalArgs = [] 392 | ): Collection { 393 | $arguments = []; 394 | $arguments['paused'] = false; // To start immediately 395 | $arguments[$metainfo ? 'metainfo' : 'filename'] = $metainfo ? base64_encode($torrent) : $torrent; 396 | 397 | if ($savepath !== null) { 398 | $arguments['download-dir'] = $savepath; 399 | } 400 | 401 | $data = $this->api('torrent-add', array_merge($arguments, $optionalArgs)); 402 | 403 | if (array_key_exists('torrent-duplicate', $data['arguments'])) { 404 | $data['arguments']['torrent-duplicate']['duplicate'] = true; 405 | 406 | return collect($data['arguments']['torrent-duplicate']); 407 | } 408 | 409 | if (!array_key_exists('torrent-added', $data['arguments'])) { 410 | throw new InvalidArgumentException($data['result']); 411 | } 412 | 413 | return collect($data['arguments']['torrent-added']); 414 | } 415 | 416 | /** 417 | * Remove one or more torrents. 418 | * 419 | * @see https://git.io/transmission-rpc-specs "torrent-remove" for available arguments. 420 | * 421 | * @param mixed $ids One or more torrent ids, as described in 3.1 of specs. 422 | * @param bool $deleteLocalData Also remove local data? (default: false). 423 | * 424 | * @return bool 425 | */ 426 | public function remove($ids, bool $deleteLocalData = false): bool 427 | { 428 | $arguments = ['ids' => $ids, 'delete-local-data' => $deleteLocalData]; 429 | $this->api('torrent-remove', $arguments); 430 | 431 | return true; 432 | } 433 | 434 | /** 435 | * Move one or more torrents to new location. 436 | * 437 | * @see https://git.io/transmission-rpc-specs "torrent-set-location" for available arguments. 438 | * 439 | * @param mixed $ids One or more torrent ids, as described in 3.1 of specs. 440 | * @param string $location The new torrent location. 441 | * @param bool $move Move from previous location or search "location" for files (default: true). 442 | * 443 | * @return bool 444 | */ 445 | public function move($ids, string $location, bool $move = true): bool 446 | { 447 | $this->api('torrent-set-location', compact('ids', 'location', 'move')); 448 | 449 | return true; 450 | } 451 | 452 | /** 453 | * Rename a Torrent's Path. 454 | * 455 | * @see https://git.io/transmission-rpc-specs "torrent-rename-path" for available arguments. 456 | * 457 | * @param mixed $ids One torrent id, as described in 3.1 of specs. 458 | * @param string $path The path to the file or folder that will be renamed. 459 | * @param string $name The file or folder's new name. 460 | * 461 | * @return array 462 | */ 463 | public function rename($ids, string $path, string $name): array 464 | { 465 | return $this->api('torrent-rename-path', compact('ids', 'path', 'name')); 466 | } 467 | 468 | /** 469 | * Set the transmission settings. 470 | * 471 | * @see https://git.io/transmission-rpc-specs "session-set" for available arguments. 472 | * 473 | * @param array $arguments one or more of spec's arguments, except: "blocklist-size", 474 | * "config-dir", "rpc-version", "rpc-version-minimum", 475 | * "version", and "session-id" 476 | * 477 | * @return bool 478 | */ 479 | public function setSettings(array $arguments): bool 480 | { 481 | $this->api('session-set', $arguments); 482 | 483 | return true; 484 | } 485 | 486 | /** 487 | * Get the transmission settings. 488 | * 489 | * @see https://git.io/transmission-rpc-specs "session-get" for available fields. 490 | * 491 | * @param array|null $fields 492 | * 493 | * @return array 494 | */ 495 | public function getSettings(array $fields = null): array 496 | { 497 | return $this->api('session-get', compact('fields')); 498 | } 499 | 500 | /** 501 | * Get Session Stats. 502 | * 503 | * @see https://git.io/transmission-rpc-specs "session-stats" for response arguments. 504 | * 505 | * @return array 506 | */ 507 | public function sessionStats(): array 508 | { 509 | return $this->api('session-stats'); 510 | } 511 | 512 | /** 513 | * Trigger Blocklist Update. 514 | * 515 | * @see https://git.io/transmission-rpc-specs "blocklist-update" for response arguments. 516 | * 517 | * @return array 518 | */ 519 | public function updateBlocklist(): array 520 | { 521 | return $this->api('blocklist-update'); 522 | } 523 | 524 | /** 525 | * Port Test: See if your incoming peer port is accessible from the outside world. 526 | * 527 | * @see https://git.io/transmission-rpc-specs "port-test" for response arguments. 528 | * 529 | * @return bool 530 | */ 531 | public function portTest(): bool 532 | { 533 | return $this->api('port-test')['arguments']['port-is-open']; 534 | } 535 | 536 | /** 537 | * Shutdown Transmission. 538 | * 539 | * @see https://git.io/transmission-rpc-specs "session-close". 540 | * 541 | * @return bool 542 | */ 543 | public function close(): bool 544 | { 545 | $this->api('session-close'); 546 | 547 | return true; 548 | } 549 | 550 | /** 551 | * Move one or more torrents to top in queue. 552 | * 553 | * @see https://git.io/transmission-rpc-specs Queue Movement Requests. 554 | * 555 | * @param mixed $ids One or more torrent ids, sha1 hash strings, or both OR "recently-active", for 556 | * recently-active torrents. All torrents are used if no value is given. 557 | * 558 | * @return bool 559 | */ 560 | public function queueMoveTop($ids = null): bool 561 | { 562 | $this->api('queue-move-top', compact('ids')); 563 | 564 | return true; 565 | } 566 | 567 | /** 568 | * Move one or more torrents up in queue. 569 | * 570 | * @see https://git.io/transmission-rpc-specs Queue Movement Requests. 571 | * 572 | * @param mixed $ids One or more torrent ids, sha1 hash strings, or both OR "recently-active", for 573 | * recently-active torrents. All torrents are used if no value is given. 574 | * 575 | * @return bool 576 | */ 577 | public function queueMoveUp($ids = null): bool 578 | { 579 | $this->api('queue-move-top', compact('ids')); 580 | 581 | return true; 582 | } 583 | 584 | /** 585 | * Move one or more torrents down in queue. 586 | * 587 | * @see https://git.io/transmission-rpc-specs Queue Movement Requests. 588 | * 589 | * @param mixed $ids One or more torrent ids, sha1 hash strings, or both OR "recently-active", for 590 | * recently-active torrents. All torrents are used if no value is given. 591 | * 592 | * @return bool 593 | */ 594 | public function queueMoveDown($ids = null): bool 595 | { 596 | $this->api('queue-move-down', compact('ids')); 597 | 598 | return true; 599 | } 600 | 601 | /** 602 | * Move one or more torrents to bottom in queue. 603 | * 604 | * @see https://git.io/transmission-rpc-specs Queue Movement Requests. 605 | * 606 | * @param mixed $ids One or more torrent ids, sha1 hash strings, or both OR "recently-active", for 607 | * recently-active torrents. All torrents are used if no value is given. 608 | * 609 | * @return bool 610 | */ 611 | public function queueMoveBottom($ids = null): bool 612 | { 613 | $this->api('queue-move-bottom', compact('ids')); 614 | 615 | return true; 616 | } 617 | 618 | /** 619 | * Free Space: Tests how much free space is available in a client-specified folder. 620 | * 621 | * @see https://git.io/transmission-rpc-specs "free-space" for arguments. 622 | * 623 | * @param null|string $path Path to check free space (default: download-dir). 624 | * 625 | * @return array 626 | */ 627 | public function freeSpace(string $path = null): array 628 | { 629 | if (blank($path)) { 630 | $path = $this->getSettings()['arguments']['download-dir']; 631 | } 632 | 633 | return $this->api('free-space', compact('path'))['arguments']; 634 | } 635 | 636 | /** 637 | * Seed Ratio Limit. 638 | * 639 | * @return int|float 640 | */ 641 | public function seedRatioLimit() 642 | { 643 | $settings = $this->getSettings(['seedRatioLimited', 'seedRatioLimit'])['arguments']; 644 | 645 | if (isset($settings['seedRatioLimited'])) { 646 | return $settings['seedRatioLimit']; 647 | } 648 | 649 | return -1; 650 | } 651 | 652 | /** 653 | * Update Download Dir. 654 | * 655 | * @param string $downloadDir Path to download torrents. 656 | * 657 | * @return bool 658 | */ 659 | public function updateDownloadDir(string $downloadDir): bool 660 | { 661 | $settings = [ 662 | 'download-dir' => $downloadDir, 663 | ]; 664 | 665 | return $this->setSettings($settings); 666 | } 667 | 668 | /** 669 | * Update & Enable Incomplete Dir. 670 | * 671 | * @param string $incompleteDir Path to store incomplete torrents. 672 | * @param bool $enableIncompleteDir Is incomplete dir enabled? (default: true). 673 | * 674 | * @return bool 675 | */ 676 | public function updateIncompleteDir(string $incompleteDir, bool $enableIncompleteDir = true): bool 677 | { 678 | $settings = [ 679 | 'incomplete-dir-enabled' => $enableIncompleteDir, 680 | 'incomplete-dir' => $incompleteDir, 681 | ]; 682 | 683 | return $this->setSettings($settings); 684 | } 685 | 686 | /** 687 | * Request API. 688 | * 689 | * @param string $method 690 | * @param array $params 691 | * 692 | * @return mixed 693 | */ 694 | protected function api(string $method, array $params = []) 695 | { 696 | $arguments = ParamBuilder::build($params); 697 | 698 | $body = json_encode(compact('method', 'arguments')); 699 | 700 | try { 701 | $response = $this->getHttpClient() 702 | ->send( 703 | 'POST', 704 | $this->transmissionUrl(), 705 | ['Content-Type' => 'application/json'], 706 | $body 707 | ); 708 | } catch (Psr18\NetworkExceptionInterface $e) { 709 | throw new NetworkException($e->getMessage(), $e->getCode()); 710 | } 711 | 712 | if (ResponseMediator::isConflictError($response)) { 713 | $this->findAndSetSessionId($response); 714 | 715 | return $this->api($method, $params); 716 | } 717 | 718 | return ResponseMediator::getContent($response); 719 | } 720 | 721 | /** 722 | * Find and Set Session ID from the response. 723 | * 724 | * @param ResponseInterface $response 725 | * 726 | * @throws TransmissionException 727 | * 728 | * @return Client 729 | */ 730 | protected function findAndSetSessionId(ResponseInterface $response): self 731 | { 732 | $sessionId = $response->getHeaderLine('x-transmission-session-id'); 733 | 734 | if (blank($sessionId)) { 735 | throw new TransmissionException('Unable to retrieve X-Transmission-Session-Id'); 736 | } 737 | 738 | $this->setSessionId($sessionId); 739 | 740 | return $this; 741 | } 742 | 743 | /** 744 | * Transmission-RPC API URL. 745 | * 746 | * @return string 747 | */ 748 | protected function transmissionUrl(): string 749 | { 750 | return 'http'.($this->isTLSEnabled() ? 's' : '').'://'.$this->host.':'.$this->port.$this->path; 751 | } 752 | 753 | /** 754 | * Default User Agent for all HTTP Requests. 755 | * 756 | * @return string HTTP User Agent. 757 | */ 758 | protected function defaultUserAgent(): string 759 | { 760 | return 'PHP-Transmission-SDK/'.self::VERSION; 761 | } 762 | 763 | /** 764 | * Get HTTP Client. 765 | * 766 | * @return Builder 767 | */ 768 | public function getHttpClient(): Builder 769 | { 770 | return $this->httpClientBuilder; 771 | } 772 | 773 | /** 774 | * @return History 775 | */ 776 | public function getResponseHistory(): History 777 | { 778 | return $this->responseHistory; 779 | } 780 | 781 | /** 782 | * @param $method 783 | * @param $arguments 784 | * 785 | * @return mixed 786 | */ 787 | public function __call($method, $arguments) 788 | { 789 | throw new \BadMethodCallException(sprintf( 790 | 'Method %s::%s does not exist.', 791 | static::class, 792 | $method 793 | )); 794 | } 795 | } 796 | -------------------------------------------------------------------------------- /src/Exception/ErrorException.php: -------------------------------------------------------------------------------- 1 | 'Bad Request', 15 | 401 => 'Unauthorized', 16 | 402 => 'Payment Required', 17 | 403 => 'Forbidden', 18 | 404 => 'Not Found', 19 | 405 => 'Method Not Allowed', 20 | 406 => 'Not Acceptable', 21 | 407 => 'Proxy Authentication Required', 22 | 408 => 'Request Timeout', 23 | 409 => 'Conflict', 24 | 410 => 'Gone', 25 | 411 => 'Length Required', 26 | 412 => 'Precondition Failed', 27 | 413 => 'Payload Too Large', 28 | 414 => 'Request-URI Too Long', 29 | 415 => 'Unsupported Media Type', 30 | 416 => 'Requested Range Not Satisfiable', 31 | 417 => 'Expectation Failed', 32 | 418 => 'I\'m a teapot', 33 | 421 => 'Misdirected Request', 34 | 422 => 'Unprocessable Entity', 35 | 423 => 'Locked', 36 | 424 => 'Failed Dependency', 37 | 426 => 'Upgrade Required', 38 | 428 => 'Precondition Required', 39 | 429 => 'Too Many Requests', 40 | 431 => 'Request Header Fields Too Large', 41 | 444 => 'Connection Closed Without Response', 42 | 451 => 'Unavailable For Legal Reasons', 43 | 499 => 'Client Closed Request', 44 | 45 | // 5xx: Server Error - The server failed to fulfill an apparently valid request. 46 | 500 => 'Internal Server Error', 47 | 501 => 'Not Implemented', 48 | 502 => 'Bad Gateway', 49 | 503 => 'Service Unavailable', 50 | 504 => 'Gateway Timeout', 51 | 505 => 'HTTP Version Not Supported', 52 | 506 => 'Variant Also Negotiates', 53 | 507 => 'Insufficient Storage', 54 | 508 => 'Loop Detected', 55 | 510 => 'Not Extended', 56 | 511 => 'Network Authentication Required', 57 | 599 => 'Network Connect Timeout Error', 58 | ]; 59 | 60 | /** 61 | * Create Exception by Network Code. 62 | * 63 | * @param int $statusCode 64 | * @param null|string $message 65 | * 66 | * @return static 67 | */ 68 | public static function createByCode(int $statusCode, string $message = null): self 69 | { 70 | $errorMessage = null; 71 | if (isset(static::$statusCodes[$statusCode])) { 72 | $errorMessage = static::$statusCodes[$statusCode]; 73 | 74 | if (filled($message)) { 75 | $errorMessage = $errorMessage.' - '.$message; 76 | } 77 | } 78 | 79 | $message = sprintf('%d: %s', $statusCode, $errorMessage ?? $message); 80 | 81 | return new static($message, $statusCode); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | $base) { 45 | $bytes /= $base; 46 | $i++; 47 | } 48 | 49 | return round($bytes, 2).' '.$units[$i]; 50 | } 51 | 52 | /** 53 | * Speed Bps. 54 | * 55 | * @param $Bps 56 | * 57 | * @return string 58 | */ 59 | public static function speedBps($Bps): string 60 | { 61 | return static::speed(static::bpsToKBps($Bps)); 62 | } 63 | 64 | /** 65 | * Bps to KBps. 66 | * 67 | * @param $Bps 68 | * 69 | * @return float 70 | */ 71 | public static function bpsToKBps($Bps): float 72 | { 73 | return floor($Bps / static::SPEED_KBPS); 74 | } 75 | 76 | /** 77 | * Format KBps to Data-rate units. 78 | * 79 | * @param $KBps 80 | * 81 | * @return string 82 | */ 83 | public static function speed($KBps): string 84 | { 85 | $speed = $KBps; 86 | 87 | if ($speed <= 999.95) { // 0 KBps to 999 K 88 | return static::trunicateNumber($speed, 0).' KB/s'; 89 | } 90 | 91 | $speed /= static::SPEED_KBPS; 92 | 93 | if ($speed <= 99.995) { // 1 M to 99.99 M 94 | return static::trunicateNumber($speed, 2).' MB/s'; 95 | } 96 | if ($speed <= 999.95) { // 100 M to 999.9 M 97 | return static::trunicateNumber($speed, 1).' MB/s'; 98 | } 99 | 100 | // insane speeds 101 | $speed /= static::SPEED_KBPS; 102 | 103 | return static::trunicateNumber($speed, 2).' GB/s'; 104 | } 105 | 106 | /** 107 | * Trunicate a number to the given decimal points. 108 | * 109 | * @param string $number 110 | * @param int $decimals 111 | * 112 | * @return string 113 | */ 114 | public static function trunicateNumber($number, int $decimals = 2): string 115 | { 116 | return bcdiv($number, 1, $decimals); 117 | } 118 | 119 | /** 120 | * Cast an attribute to a native PHP type. 121 | * 122 | * @param mixed $value 123 | * @param string $type 124 | * 125 | * @return mixed 126 | */ 127 | public static function castAttribute($type, $value) 128 | { 129 | if ($value === null) { 130 | return $value; 131 | } 132 | 133 | switch ($type) { 134 | case 'collection': 135 | return collect(\is_array($value) ? $value : (new static())->fromJson($value)); 136 | case 'interval': 137 | return $value < 1 ? -1 : CarbonInterval::seconds($value)->cascade(); 138 | case 'date': 139 | return (new static())->asDate($value); 140 | case 'datetime': 141 | return (new static())->asDateTime($value); 142 | case 'timestamp': 143 | return (new static())->asTimestamp($value); 144 | case 'size': 145 | return static::formatBytes($value); 146 | case 'memory': 147 | return static::formatBytes($value, true, static::UNITS_MODE_BINARY); 148 | case 'datarate': 149 | return static::speedBps($value); 150 | default: 151 | return $value; 152 | } 153 | } 154 | 155 | /** 156 | * Encode the given value as JSON. 157 | * 158 | * @param mixed $value 159 | * 160 | * @return string 161 | */ 162 | protected function asJson($value): string 163 | { 164 | return json_encode($value); 165 | } 166 | 167 | /** 168 | * Decode the given JSON back into an array or object. 169 | * 170 | * @param string $value 171 | * @param bool $asObject 172 | * 173 | * @return mixed 174 | */ 175 | protected function fromJson($value, $asObject = false) 176 | { 177 | return json_decode($value, !$asObject); 178 | } 179 | 180 | /** 181 | * Return a timestamp as DateTime object with time set to 00:00:00. 182 | * 183 | * @param mixed $value 184 | * 185 | * @return \Illuminate\Support\Carbon 186 | */ 187 | protected function asDate($value): Carbon 188 | { 189 | return $this->asDateTime($value)->startOfDay(); 190 | } 191 | 192 | /** 193 | * Return a timestamp as DateTime object. 194 | * 195 | * @param mixed $value 196 | * 197 | * @return \Illuminate\Support\Carbon 198 | */ 199 | protected function asDateTime($value): Carbon 200 | { 201 | // If this value is already a Carbon instance, we shall just return it as is. 202 | // This prevents us having to re-instantiate a Carbon instance when we know 203 | // it already is one, which wouldn't be fulfilled by the DateTime check. 204 | if ($value instanceof Carbon) { 205 | return $value; 206 | } 207 | 208 | // If this value is an integer, we will assume it is a UNIX timestamp's value 209 | // and format a Carbon object from this timestamp. This allows flexibility 210 | // when defining your date fields as they might be UNIX timestamps here. 211 | if (is_numeric($value)) { 212 | return Carbon::createFromTimestamp($value); 213 | } 214 | 215 | return $value; 216 | } 217 | 218 | /** 219 | * Return a timestamp as unix timestamp. 220 | * 221 | * @param mixed $value 222 | * 223 | * @return int 224 | */ 225 | protected function asTimestamp($value): int 226 | { 227 | return $this->asDateTime($value)->getTimestamp(); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/HttpClient/Builder.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class Builder 19 | { 20 | /** 21 | * The object that sends HTTP messages. 22 | * 23 | * @var ClientInterface 24 | */ 25 | private $httpClient; 26 | 27 | /** 28 | * A HTTP client with all our plugins. 29 | * 30 | * @var ClientInterface 31 | */ 32 | private $pluginClient; 33 | 34 | /** 35 | * @var RequestBuilder 36 | */ 37 | private $requestBuilder; 38 | 39 | /** 40 | * True if we should create a new Plugin client at next request. 41 | * 42 | * @var bool 43 | */ 44 | private $httpClientModified = true; 45 | 46 | /** 47 | * @var Plugin[] 48 | */ 49 | private $plugins = []; 50 | 51 | /** 52 | * @param ClientInterface $httpClient The client to send requests with. 53 | */ 54 | public function __construct(ClientInterface $httpClient = null) 55 | { 56 | $this->httpClient = $httpClient ?? Psr18ClientDiscovery::find(); 57 | $this->requestBuilder = new RequestBuilder(); 58 | } 59 | 60 | /** 61 | * @return ClientInterface 62 | */ 63 | public function getHttpClient(): ClientInterface 64 | { 65 | if ($this->httpClientModified) { 66 | $this->httpClientModified = false; 67 | 68 | $this->pluginClient = (new PluginClientFactory())->createClient($this->httpClient, $this->plugins); 69 | } 70 | 71 | return $this->pluginClient; 72 | } 73 | 74 | /** 75 | * Sets the http client. 76 | * 77 | * @param ClientInterface $httpClient 78 | * 79 | * @return Builder 80 | */ 81 | public function setHttpClient(ClientInterface $httpClient): self 82 | { 83 | $this->httpClient = $httpClient; 84 | 85 | return $this; 86 | } 87 | 88 | /** 89 | * @return RequestBuilder 90 | */ 91 | public function getRequestBuilder(): RequestBuilder 92 | { 93 | return $this->requestBuilder; 94 | } 95 | 96 | /** 97 | * @param RequestBuilder $requestBuilder 98 | * 99 | * @return Builder 100 | */ 101 | public function setRequestBuilder(RequestBuilder $requestBuilder): self 102 | { 103 | $this->requestBuilder = $requestBuilder; 104 | 105 | return $this; 106 | } 107 | 108 | /** 109 | * Add a new plugin to the end of the plugin chain. 110 | * 111 | * @param Plugin $plugin 112 | * 113 | * @return Builder 114 | */ 115 | public function addPlugin(Plugin $plugin): self 116 | { 117 | $this->plugins[] = $plugin; 118 | $this->httpClientModified = true; 119 | 120 | return $this; 121 | } 122 | 123 | /** 124 | * Remove a plugin by its fully qualified class name (FQCN). 125 | * 126 | * @param string $fqcn 127 | * 128 | * @return Builder 129 | */ 130 | public function removePlugin($fqcn): self 131 | { 132 | foreach ($this->plugins as $idx => $plugin) { 133 | if ($plugin instanceof $fqcn) { 134 | unset($this->plugins[$idx]); 135 | $this->httpClientModified = true; 136 | } 137 | } 138 | 139 | return $this; 140 | } 141 | 142 | /** 143 | * @param string $method 144 | * @param $uri 145 | * @param array $headers 146 | * @param null $body 147 | * 148 | * @throws \Psr\Http\Client\ClientExceptionInterface 149 | * 150 | * @return ResponseInterface 151 | */ 152 | public function send(string $method, $uri, array $headers = [], $body = null): ResponseInterface 153 | { 154 | return $this->sendRequest($this->requestBuilder->create( 155 | $method, 156 | $uri, 157 | $headers, 158 | $body 159 | )); 160 | } 161 | 162 | /** 163 | * @param RequestInterface $request 164 | * 165 | * @throws \Psr\Http\Client\ClientExceptionInterface 166 | * 167 | * @return ResponseInterface 168 | */ 169 | public function sendRequest(RequestInterface $request): ResponseInterface 170 | { 171 | return $this->getHttpClient()->sendRequest($request); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/HttpClient/Message/ParamBuilder.php: -------------------------------------------------------------------------------- 1 | reject(function ($value) { 21 | return blank($value); 22 | })->transform(function ($value) { 23 | if (is_object($value)) { 24 | return $value->toArray(); 25 | } 26 | 27 | if (is_array($value)) { 28 | return static::build($value); 29 | } 30 | 31 | if (is_numeric($value)) { 32 | return $value + 0; 33 | } 34 | 35 | if (is_bool($value)) { 36 | return (int) $value; 37 | } 38 | 39 | if (is_string($value) && mb_detect_encoding($value, 'auto') !== 'UTF-8') { 40 | return mb_convert_encoding($value, 'UTF-8'); 41 | } 42 | 43 | return $value; 44 | })->toArray(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/HttpClient/Message/ResponseMediator.php: -------------------------------------------------------------------------------- 1 | getStatusCode() >= 100 && $response->getStatusCode() < 200; 22 | } 23 | 24 | /** 25 | * Check status code for success. 26 | * 27 | * @param ResponseInterface $response 28 | * 29 | * @return bool 30 | */ 31 | public static function isSuccess(ResponseInterface $response): bool 32 | { 33 | return $response->getStatusCode() >= 200 && $response->getStatusCode() < 300; 34 | } 35 | 36 | /** 37 | * Alias of static::isSuccess(). 38 | * 39 | * @see static::isSuccess() 40 | * 41 | * @param ResponseInterface $response 42 | * 43 | * @return bool 44 | */ 45 | public static function isOk(ResponseInterface $response): bool 46 | { 47 | return static::isSuccess($response); 48 | } 49 | 50 | /** 51 | * Check status code for redirect. 52 | * 53 | * @param ResponseInterface $response 54 | * 55 | * @return bool 56 | */ 57 | public static function isRedirect(ResponseInterface $response): bool 58 | { 59 | return $response->getStatusCode() >= 300 && $response->getStatusCode() < 400; 60 | } 61 | 62 | /** 63 | * Check status code for errors. 64 | * 65 | * @see static::isSuccess() 66 | * 67 | * @param ResponseInterface $response 68 | * 69 | * @return bool 70 | */ 71 | public static function isError(ResponseInterface $response): bool 72 | { 73 | return static::isClientError($response) || static::isServerError($response); 74 | } 75 | 76 | /** 77 | * Check status code for client error. 78 | * 79 | * @param ResponseInterface $response 80 | * 81 | * @return bool 82 | */ 83 | public static function isClientError(ResponseInterface $response): bool 84 | { 85 | return $response->getStatusCode() >= 400 && $response->getStatusCode() < 500; 86 | } 87 | 88 | /** 89 | * Check status code for server error. 90 | * 91 | * @param ResponseInterface $response 92 | * 93 | * @return bool 94 | */ 95 | public static function isServerError(ResponseInterface $response): bool 96 | { 97 | return $response->getStatusCode() >= 500; 98 | } 99 | 100 | /** 101 | * Check status code for conflict error. 102 | * 103 | * @param ResponseInterface $response 104 | * 105 | * @return bool 106 | */ 107 | public static function isConflictError(ResponseInterface $response): bool 108 | { 109 | return $response->getStatusCode() === 409; 110 | } 111 | 112 | /** 113 | * Return the response body as a string or json array if content type is application/json. 114 | * 115 | * @param ResponseInterface $response 116 | * 117 | * @return array|string 118 | */ 119 | public static function getContent(ResponseInterface $response) 120 | { 121 | $body = $response->getBody()->__toString(); 122 | 123 | if (static::isError($response)) { 124 | // Transmission returns error messages with HTML. 125 | return strip_tags($body); 126 | } 127 | 128 | if (strpos($response->getHeaderLine('Content-Type'), 'application/json') === 0) { 129 | $content = json_decode($body, true); 130 | if (JSON_ERROR_NONE === json_last_error()) { 131 | return $content; 132 | } 133 | } 134 | 135 | return $body; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/HttpClient/Plugin/AuthSession.php: -------------------------------------------------------------------------------- 1 | sessionId = $sessionId; 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise 29 | { 30 | if (filled($this->sessionId)) { 31 | $request = $request->withHeader('X-Transmission-Session-Id', $this->sessionId); 32 | } 33 | 34 | return $next($request); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/HttpClient/Plugin/ExceptionThrower.php: -------------------------------------------------------------------------------- 1 | then(function (ResponseInterface $response) { 24 | $statusCode = $response->getStatusCode(); 25 | $content = ResponseMediator::getContent($response); 26 | 27 | if (ResponseMediator::isSuccess($response)) { 28 | if (is_array($content) && isset($content['result']) && 'success' !== $content['result']) { 29 | throw new TransmissionException($content['result'], $statusCode); 30 | } 31 | } elseif (!ResponseMediator::isConflictError($response) && ResponseMediator::isError($response)) { 32 | switch ($statusCode) { 33 | case 401: 34 | $message = 'Invalid Username/Password'; 35 | break; 36 | case 403: 37 | $message = 'Your IP Address is Not Whitelisted'; 38 | break; 39 | default: 40 | $message = is_array($content) ? $content['result'] : $content; 41 | break; 42 | } 43 | 44 | throw NetworkException::createByCode($statusCode, $message); 45 | } 46 | 47 | return $response; 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/HttpClient/Plugin/History.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class History implements Journal 16 | { 17 | /** 18 | * @var ResponseInterface 19 | */ 20 | private $lastResponse; 21 | 22 | /** 23 | * @return ResponseInterface|null 24 | */ 25 | public function getLastResponse() 26 | { 27 | return $this->lastResponse; 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function addSuccess(RequestInterface $request, ResponseInterface $response) 34 | { 35 | $this->lastResponse = $response; 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function addFailure(RequestInterface $request, ClientExceptionInterface $exception) 42 | { 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/HttpClient/RequestBuilder.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class RequestBuilder 19 | { 20 | /** 21 | * @var RequestFactoryInterface|null 22 | */ 23 | private $requestFactory; 24 | 25 | /** 26 | * @var StreamFactoryInterface|null 27 | */ 28 | private $streamFactory; 29 | 30 | /** 31 | * Creates a new PSR-7 request. 32 | * 33 | * @param string $method 34 | * @param string $uri 35 | * @param array $headers 36 | * @param array|string|null $body Request body. 37 | * 38 | * @return RequestInterface 39 | */ 40 | public function create(string $method, string $uri, array $headers = [], $body = null): RequestInterface 41 | { 42 | $stream = $this->getStreamFactory()->createStream($body); 43 | 44 | return $this->createRequest($method, $uri, $headers, $stream); 45 | } 46 | 47 | private function getRequestFactory(): RequestFactoryInterface 48 | { 49 | if (null === $this->requestFactory) { 50 | $this->requestFactory = Psr17FactoryDiscovery::findRequestFactory(); 51 | } 52 | 53 | return $this->requestFactory; 54 | } 55 | 56 | public function setRequestFactory(RequestFactoryInterface $requestFactory): self 57 | { 58 | $this->requestFactory = $requestFactory; 59 | 60 | return $this; 61 | } 62 | 63 | private function getStreamFactory(): StreamFactoryInterface 64 | { 65 | if (null === $this->streamFactory) { 66 | $this->streamFactory = Psr17FactoryDiscovery::findStreamFactory(); 67 | } 68 | 69 | return $this->streamFactory; 70 | } 71 | 72 | public function setStreamFactory(StreamFactoryInterface $streamFactory): self 73 | { 74 | $this->streamFactory = $streamFactory; 75 | 76 | return $this; 77 | } 78 | 79 | private function createRequest(string $method, string $uri, array $headers, StreamInterface $stream) 80 | { 81 | $request = $this->getRequestFactory()->createRequest($method, $uri); 82 | $request = $request->withBody($stream); 83 | foreach ($headers as $name => $value) { 84 | $request = $request->withAddedHeader($name, $value); 85 | } 86 | 87 | return $request; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Laravel/Facade.php: -------------------------------------------------------------------------------- 1 | app instanceof LaravelApplication && $this->app->runningInConsole()) { 22 | $this->publishes([ 23 | __DIR__.'/config/transmission.php' => config_path('transmission.php'), 24 | ]); 25 | } elseif ($this->app instanceof LumenApplication) { 26 | $this->app->configure('transmission'); 27 | } 28 | } 29 | 30 | /** 31 | * Register the Service. 32 | * 33 | * @return void 34 | */ 35 | public function register() 36 | { 37 | $this->mergeConfigFrom(__DIR__.'/config/transmission.php', 'transmission'); 38 | 39 | $this->app->singleton('transmission', function () { 40 | $client = new Client( 41 | config('transmission.host'), 42 | config('transmission.port'), 43 | config('transmission.username'), 44 | config('transmission.password') 45 | ); 46 | 47 | if (config('transmission.enableTLS')) { 48 | $client = $client->enableTLS(); 49 | } 50 | 51 | return $client; 52 | }); 53 | 54 | $this->app->alias('transmission', Client::class); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Laravel/config/transmission.php: -------------------------------------------------------------------------------- 1 | env('TRANSMISSION_ENABLE_TLS', false), 14 | 'host' => env('TRANSMISSION_HOST', '127.0.0.1'), 15 | 'port' => env('TRANSMISSION_PORT', 9091), 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Transmission-RPC Authentication 20 | |-------------------------------------------------------------------------- 21 | | 22 | | If authentication is enabled, provide your username and password. 23 | | 24 | */ 25 | 26 | 'username' => env('TRANSMISSION_USERNAME', ''), 27 | 'password' => env('TRANSMISSION_PASSWORD', ''), 28 | 29 | ]; 30 | -------------------------------------------------------------------------------- /src/Models/AbstractModel.php: -------------------------------------------------------------------------------- 1 | castAttribute($key, $value, $castingEnabled ?? $this->castingEnabled); 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function toArray() 46 | { 47 | $this->castAllAttributes(); 48 | 49 | return parent::toArray(); 50 | } 51 | 52 | /** 53 | * Enable casting attributes globally. 54 | * 55 | * @param bool $castingEnabled Default enable casting. 56 | * 57 | * @return $this 58 | */ 59 | public function enableCasting($castingEnabled = true): self 60 | { 61 | $this->castingEnabled = $castingEnabled; 62 | 63 | return $this; 64 | } 65 | 66 | /** 67 | * Cast All Attributes. 68 | */ 69 | protected function castAllAttributes(): void 70 | { 71 | $this->transform(function ($value, $key) { 72 | return $this->castAttribute($key, $value, $this->castingEnabled); 73 | }); 74 | } 75 | 76 | /** 77 | * @param string $key 78 | * @param mixed $value 79 | * @param bool $castingEnabled 80 | * 81 | * @return mixed 82 | */ 83 | protected function castAttribute(string $key, $value, bool $castingEnabled = false) 84 | { 85 | if ($castingEnabled && array_key_exists($key, $this->casts)) { 86 | return Formatter::castAttribute($this->casts[$key], $value); 87 | } 88 | 89 | return $value; 90 | } 91 | 92 | /** 93 | * Magic method to get attributes dynamically. 94 | * 95 | * @param $method 96 | * @param $arguments 97 | * 98 | * @return mixed 99 | */ 100 | public function __call($method, $arguments) 101 | { 102 | $attribute = Str::camel(Str::after($method, 'get')); 103 | if (!Str::startsWith($method, 'get') || !$this->has($attribute)) { 104 | throw new \BadMethodCallException(sprintf( 105 | 'Method %s::%s does not exist.', 106 | static::class, 107 | $method 108 | )); 109 | } 110 | 111 | $castingEnabled = $arguments[0] ?? $this->castingEnabled; 112 | $value = parent::get($attribute); 113 | 114 | return $this->castAttribute($attribute, $value, $castingEnabled); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Models/Torrent.php: -------------------------------------------------------------------------------- 1 | announceState 58 | */ 59 | public const TRACKER_INACTIVE = 0; 60 | public const TRACKER_WAITING = 1; 61 | public const TRACKER_QUEUED = 2; 62 | public const TRACKER_ACTIVE = 3; 63 | 64 | /** 65 | * Common Fields. 66 | * 67 | * @var array 68 | */ 69 | public static $fields = [ 70 | 'default' => [ 71 | 'id', 72 | 'eta', 73 | 'name', 74 | 'status', 75 | 'isFinished', 76 | 'files', 77 | 'hashString', 78 | 'downloadDir', 79 | 'percentDone', 80 | 'haveValid', 81 | 'haveUnchecked', 82 | 'totalSize', 83 | 'leftUntilDone', 84 | 'addedDate', 85 | 'doneDate', 86 | 'activityDate', 87 | ], 88 | 'stats' => [ 89 | 'error', 90 | 'errorString', 91 | 'eta', 92 | 'isFinished', 93 | 'isStalled', 94 | 'leftUntilDone', 95 | 'metadataPercentComplete', 96 | 'peersConnected', 97 | 'peersGettingFromUs', 98 | 'peersSendingToUs', 99 | 'percentDone', 100 | 'queuePosition', 101 | 'rateDownload', 102 | 'rateUpload', 103 | 'recheckProgress', 104 | 'seedRatioMode', 105 | 'seedRatioLimit', 106 | 'sizeWhenDone', 107 | 'status', 108 | 'trackers', 109 | 'downloadDir', 110 | 'uploadedEver', 111 | 'uploadRatio', 112 | 'webseedsSendingToUs', 113 | ], 114 | 'statsExtra' => [ 115 | 'activityDate', 116 | 'corruptEver', 117 | 'desiredAvailable', 118 | 'downloadedEver', 119 | 'fileStats', 120 | 'haveUnchecked', 121 | 'haveValid', 122 | 'peers', 123 | 'startDate', 124 | 'trackerStats', 125 | ], 126 | 'infoExtra' => [ 127 | 'comment', 128 | 'creator', 129 | 'dateCreated', 130 | 'files', 131 | 'hashString', 132 | 'isPrivate', 133 | 'pieceCount', 134 | 'pieceSize', 135 | ], 136 | ]; 137 | 138 | /** 139 | * The attributes that should be cast to native and other supported types. 140 | * 141 | * Casts only when formatting is enabled. 142 | * 143 | * @var array 144 | */ 145 | protected $casts = [ 146 | 'doneDate' => 'datetime', 147 | 'startDate' => 'datetime', 148 | 'activityDate' => 'datetime', 149 | 'addedDate' => 'datetime', 150 | 'dateCreated' => 'datetime', 151 | 'eta' => 'interval', 152 | 'haveValid' => 'size', 153 | 'haveUnchecked' => 'size', 154 | 'totalDone' => 'size', // Custom 155 | 'leftUntilDone' => 'size', 156 | 'totalSize' => 'size', 157 | 'sizeWhenDone' => 'size', 158 | 'uploadedEver' => 'size', 159 | 'rateDownload' => 'datarate', 160 | 'rateUpload' => 'datarate', 161 | ]; 162 | 163 | /** 164 | * Get Name. 165 | * 166 | * @return string 167 | */ 168 | public function getName(): string 169 | { 170 | return $this->get('name', 'Unknown'); 171 | } 172 | 173 | /** 174 | * Get Percent Done. 175 | * 176 | * @param bool $format 177 | * 178 | * @return int 179 | */ 180 | public function getPercentDone($format = false): int 181 | { 182 | $percentDone = $this->get('percentDone', 0); 183 | 184 | return $format ? $percentDone * 100 : $percentDone; 185 | } 186 | 187 | /** 188 | * Get Percent Done String. 189 | * 190 | * @return string 191 | */ 192 | public function getPercentDoneString(): string 193 | { 194 | return $this->getPercentDone(true).'%'; 195 | } 196 | 197 | /** 198 | * Get Metadata Percent Complete. 199 | * 200 | * @param bool $format 201 | * 202 | * @return int 203 | */ 204 | public function getMetadataPercentComplete($format = false): int 205 | { 206 | $percent = $this->get('metadataPercentComplete', 0); 207 | 208 | return $format ? $percent * 100 : $percent; 209 | } 210 | 211 | /** 212 | * Get Recheck Progress Percent. 213 | * 214 | * @param bool $format 215 | * 216 | * @return int 217 | */ 218 | public function getRecheckProgress($format = false): int 219 | { 220 | $percent = $this->get('recheckProgress', 0); 221 | 222 | return $format ? $percent * 100 : $percent; 223 | } 224 | 225 | /** 226 | * Get Total Done. 227 | * 228 | * @param null|bool $castingEnabled 229 | * 230 | * @return mixed 231 | */ 232 | public function getTotalDone($castingEnabled = null) 233 | { 234 | $value = $this->getHaveValid(false) + $this->getHaveUnchecked(false); 235 | 236 | return $this->castAttribute('totalDone', $value, $castingEnabled ?? $this->castingEnabled); 237 | } 238 | 239 | /** 240 | * Get Upload Speed. 241 | * 242 | * @param null|bool $castingEnabled 243 | * 244 | * @return mixed 245 | */ 246 | public function getUploadSpeed($castingEnabled = null) 247 | { 248 | return $this->get('rateUpload', 0, $castingEnabled); 249 | } 250 | 251 | /** 252 | * Get Download Speed. 253 | * 254 | * @param null|bool $castingEnabled 255 | * 256 | * @return mixed 257 | */ 258 | public function getDownloadSpeed($castingEnabled = null) 259 | { 260 | return $this->get('rateDownload', 0, $castingEnabled); 261 | } 262 | 263 | /** 264 | * Get File Count. 265 | * 266 | * @return mixed 267 | */ 268 | public function getFileCount() 269 | { 270 | return \count($this->get('files', 0)); 271 | } 272 | 273 | /** 274 | * Get a File by ID. 275 | * 276 | * @param int $id 277 | * 278 | * @return mixed 279 | */ 280 | public function getFile(int $id) 281 | { 282 | return data_get($this->items, "files.$id"); 283 | } 284 | 285 | /** 286 | * Check if status is stopped. 287 | * 288 | * @return bool 289 | */ 290 | public function isStopped(): bool 291 | { 292 | return $this->isStatus(static::STATUS_STOPPED); 293 | } 294 | 295 | /** 296 | * Check if status is checking. 297 | * 298 | * @return bool 299 | */ 300 | public function isChecking(): bool 301 | { 302 | return $this->isStatus(static::STATUS_CHECK); 303 | } 304 | 305 | /** 306 | * Check if status is downloading. 307 | * 308 | * @return bool 309 | */ 310 | public function isDownloading(): bool 311 | { 312 | return $this->isStatus(static::STATUS_DOWNLOAD); 313 | } 314 | 315 | /** 316 | * Check if status is queued. 317 | * 318 | * @return bool 319 | */ 320 | public function isQueued(): bool 321 | { 322 | return $this->isStatus(static::STATUS_DOWNLOAD_WAIT) || $this->isStatus(static::STATUS_SEED_WAIT); 323 | } 324 | 325 | /** 326 | * Check if status is seeding. 327 | * 328 | * @return bool 329 | */ 330 | public function isSeeding(): bool 331 | { 332 | return $this->isStatus(static::STATUS_SEED); 333 | } 334 | 335 | /** 336 | * Check if done downloading. 337 | * 338 | * @return bool 339 | */ 340 | public function isDone(): bool 341 | { 342 | return $this->getLeftUntilDone(false) < 1; 343 | } 344 | 345 | /** 346 | * Check if given status matches the current status. 347 | * 348 | * @param $status 349 | * 350 | * @return bool 351 | */ 352 | public function isStatus($status): bool 353 | { 354 | return $this->get('status') === $status; 355 | } 356 | 357 | /** 358 | * Check if meta data needs to be complete. 359 | * 360 | * @return bool 361 | */ 362 | public function needsMetaData(): bool 363 | { 364 | return $this->getMetadataPercentComplete() < 1; 365 | } 366 | 367 | /** 368 | * Determine it's a magnet. 369 | * 370 | * @return bool 371 | */ 372 | public function isMagnet(): bool 373 | { 374 | return $this->needsMetaData(); 375 | } 376 | 377 | /** 378 | * Determine it's a multi-file folder. 379 | * 380 | * @return bool 381 | */ 382 | public function isFolder(): bool 383 | { 384 | return $this->getFileCount() > 1; 385 | } 386 | 387 | /** 388 | * Get Status String. 389 | * 390 | * @return string 391 | */ 392 | public function getStatusString(): ?string 393 | { 394 | switch ($this->get('status')) { 395 | case static::STATUS_STOPPED: 396 | return $this->get('isFinished', false) ? 'Seeding complete' : 'Paused'; 397 | case static::STATUS_CHECK_WAIT: 398 | return 'Queued for verification'; 399 | case static::STATUS_CHECK: 400 | return 'Verifying local data'; 401 | case static::STATUS_DOWNLOAD_WAIT: 402 | return 'Queued for download'; 403 | case static::STATUS_DOWNLOAD: 404 | return 'Downloading'; 405 | case static::STATUS_SEED_WAIT: 406 | return 'Queued for seeding'; 407 | case static::STATUS_SEED: 408 | return 'Seeding'; 409 | case null: 410 | return 'Unknown'; 411 | default: 412 | return 'Error'; 413 | } 414 | } 415 | 416 | /** 417 | * Get Seed Ratio Limit. 418 | * 419 | * @param int|Client $globalSeedRatioLimit Provide the global seed ratio limit if you already have cached. This is 420 | * to prevent fetching on every request when looping through multiple 421 | * torrents as it'll be very slow doing so. It's recommended to cache it 422 | * once and pass to this method, otherwise provide Client instance. 423 | * 424 | * @return int|string 425 | */ 426 | public function seedRatioLimit($globalSeedRatioLimit) 427 | { 428 | switch ($this->get('seedRatioMode')) { 429 | case static::RATIO_USE_GLOBAL: 430 | return ($globalSeedRatioLimit instanceof Client) ? $globalSeedRatioLimit->seedRatioLimit() : $globalSeedRatioLimit; 431 | case static::RATIO_USE_LOCAL: 432 | return $this->get('seedRatioLimit'); 433 | default: 434 | return -1; 435 | } 436 | } 437 | 438 | /** 439 | * Get Error Message. 440 | * 441 | * @return string|null 442 | */ 443 | public function getErrorMessage(): ?string 444 | { 445 | $str = $this->get('errorString'); 446 | switch ($this->get('error')) { 447 | case static::ERROR_TRACKER_WARNING: 448 | return 'Tracker returned a warning: '.$str; 449 | case static::ERROR_TRACKER_ERROR: 450 | return 'Tracker returned an error: '.$str; 451 | case static::ERROR_LOCAL_ERROR: 452 | return 'Error: '.$str; 453 | default: 454 | return null; 455 | } 456 | } 457 | } 458 | --------------------------------------------------------------------------------