Passed
Push — master ( aebc3a...07e708 )
by Irfaq
14:17
created

Client::updateDownloadDir()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 7
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
3
namespace Transmission;
4
5
use Http\Client\Common\HttpMethodsClient;
6
use Http\Client\Common\Plugin\AuthenticationPlugin;
7
use Http\Client\Common\Plugin\HeaderDefaultsPlugin;
8
use Http\Client\Common\Plugin\HistoryPlugin;
9
use Http\Client\HttpClient;
10
use Http\Message\Authentication\BasicAuth;
11
use Illuminate\Support\Collection;
12
use Psr\Http\Message\ResponseInterface;
13
use Transmission\Exception\InvalidArgumentException;
14
use Transmission\Exception\TransmissionException;
15
use Transmission\HttpClient\Builder;
16
use Transmission\HttpClient\Message\ParamBuilder;
17
use Transmission\HttpClient\Message\ResponseMediator;
18
use Transmission\HttpClient\Plugin\AuthSession;
19
use Transmission\HttpClient\Plugin\ExceptionThrower;
20
use Transmission\HttpClient\Plugin\History;
21
use Transmission\Models\Torrent;
22
23
/**
24
 * Transmission-RPC API SDK Client
25
 */
26
class Client
27
{
28
    /**
29
     * SDK Version
30
     *
31
     * @var string
32
     */
33
    const VERSION = '1.1.0';
34
35
    /**
36
     * Transmission-RPC Hostname
37
     *
38
     * @var string
39
     */
40
    public $hostname;
41
42
    /**
43
     * Transmission-RPC Port
44
     *
45
     * @var string
46
     */
47
    public $port;
48
49
    /**
50
     * Transmission-RPC Path
51
     *
52
     * @var string
53
     */
54
    public $path = '/transmission/rpc';
55
56
    /**
57
     * @var History
58
     */
59
    private $responseHistory;
60
61
    /**
62
     * @var Builder
63
     */
64
    private $httpClientBuilder;
65
66
    /**
67
     * Instantiate a new Transmission Client.
68
     *
69
     * @param null|string  $hostname
70
     * @param null|int     $port
71
     * @param null|string  $username
72
     * @param null|string  $password
73
     * @param Builder|null $httpClientBuilder
74
     */
75
    public function __construct(
76
        string $hostname = null,
77
        int $port = null,
78
        string $username = null,
79
        string $password = null,
80
        Builder $httpClientBuilder = null
81
    ) {
82
        $this->hostname = $hostname ?? env('TRANSMISSION_HOSTNAME', '127.0.0.1');
1 ignored issue
show
Documentation Bug introduced by
It seems like $hostname ?? env('TRANSM...HOSTNAME', '127.0.0.1') can also be of type boolean. However, the property $hostname is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
83
        $this->port = $port ?? env('TRANSMISSION_PORT', 9091);
1 ignored issue
show
Documentation Bug introduced by
It seems like $port ?? env('TRANSMISSION_PORT', 9091) can also be of type boolean. However, the property $port is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
84
85
        $this->responseHistory = new History();
86
        $this->httpClientBuilder = $httpClientBuilder ?? new Builder();
87
        $this->httpClientBuilder->addPlugin(new ExceptionThrower());
88
        $this->httpClientBuilder->addPlugin(new HistoryPlugin($this->responseHistory));
89
        $this->httpClientBuilder->addPlugin(new HeaderDefaultsPlugin([
90
            'User-Agent' => $this->defaultUserAgent(),
91
        ]));
92
93
        $username = $username ?? env('TRANSMISSION_USERNAME');
94
        $password = $password ?? env('TRANSMISSION_PASSWORD', '');
95
96
        if (filled($username)) {
97
            $this->authenticate($username, $password);
2 ignored issues
show
Bug introduced by
It seems like $username can also be of type null and boolean; however, parameter $username of Transmission\Client::authenticate() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

97
            $this->authenticate(/** @scrutinizer ignore-type */ $username, $password);
Loading history...
Bug introduced by
It seems like $password can also be of type null and boolean; however, parameter $password of Transmission\Client::authenticate() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

97
            $this->authenticate($username, /** @scrutinizer ignore-type */ $password);
Loading history...
98
        }
99
    }
100
101
    /**
102
     * Create a Transmission\Client.
103
     *
104
     * @param null|string $hostname
105
     * @param null|int    $port
106
     * @param null|string $username
107
     * @param null|string $password
108
     *
109
     * @return Client
110
     */
111
    public static function create(
112
        string $hostname = null,
113
        int $port = null,
114
        string $username = null,
115
        string $password = null
116
    ): self {
117
        $client = new static($hostname, $port, $username, $password);
118
119
        return $client;
120
    }
121
122
    /**
123
     * Create a Transmission\Client using an HttpClient.
124
     *
125
     * @param HttpClient  $httpClient
126
     * @param null|string $hostname
127
     * @param null|int    $port
128
     * @param null|string $username
129
     * @param null|string $password
130
     *
131
     * @return Client
132
     */
133
    public static function createWithHttpClient(
134
        HttpClient $httpClient,
135
        string $hostname = null,
136
        int $port = null,
137
        string $username = null,
138
        string $password = null
139
    ): self {
140
        $builder = new Builder($httpClient);
141
142
        return new static($hostname, $port, $username, $password, $builder);
143
    }
144
145
    /**
146
     * Get Client Instance.
147
     *
148
     * @return Client
149
     */
150
    public function instance(): self
151
    {
152
        return $this;
153
    }
154
155
    /**
156
     * Authenticate the user for all next requests
157
     *
158
     * @param string      $username
159
     * @param null|string $password
160
     *
161
     * @return Client
162
     */
163
    public function authenticate(string $username, string $password = ''): self
164
    {
165
        $authentication = new BasicAuth($username, $password);
166
167
        $this->httpClientBuilder->removePlugin(AuthenticationPlugin::class);
168
        $this->httpClientBuilder->addPlugin(new AuthenticationPlugin($authentication));
169
170
        return $this;
171
    }
172
173
    /**
174
     * Set Session ID.
175
     *
176
     * @param string $sessionId
177
     *
178
     * @return Client
179
     */
180
    public function setSessionId(string $sessionId): self
181
    {
182
        $this->httpClientBuilder->removePlugin(AuthSession::class);
183
        $this->httpClientBuilder->addPlugin(new AuthSession($sessionId));
184
185
        return $this;
186
    }
187
188
    /**
189
     * Start one or more torrents.
190
     *
191
     * @see https://git.io/transmission-rpc-specs Torrent Action Requests.
192
     *
193
     * @param mixed $ids One or more torrent ids, sha1 hash strings, or both OR "recently-active", for recently-active
194
     *                   torrents. All torrents are used if no value is given.
195
     *
196
     * @return bool
197
     */
198
    public function start($ids = null): bool
199
    {
200
        $this->api('torrent-start', compact('ids'));
201
202
        return true;
203
    }
204
205
    /**
206
     * Start Now one or more torrents.
207
     *
208
     * @see https://git.io/transmission-rpc-specs Torrent Action Requests.
209
     *
210
     * @param mixed $ids One or more torrent ids, as described in 3.1 of specs.
211
     *
212
     * @return bool
213
     */
214
    public function startNow($ids = null): bool
215
    {
216
        $this->api('torrent-start-now', compact('ids'));
217
218
        return true;
219
    }
220
221
    /**
222
     * Stop one or more torrents.
223
     *
224
     * @see https://git.io/transmission-rpc-specs Torrent Action Requests.
225
     *
226
     * @param mixed $ids One or more torrent ids, as described in 3.1 of specs.
227
     *
228
     * @return bool
229
     */
230
    public function stop($ids = null): bool
231
    {
232
        $this->api('torrent-stop', compact('ids'));
233
234
        return true;
235
    }
236
237
    /**
238
     * Verify one or more torrents.
239
     *
240
     * @see https://git.io/transmission-rpc-specs Torrent Action Requests.
241
     *
242
     * @param mixed $ids One or more torrent ids, as described in 3.1 of specs.
243
     *
244
     * @return bool
245
     */
246
    public function verify($ids = null): bool
247
    {
248
        $this->api('torrent-verify', compact('ids'));
249
250
        return true;
251
    }
252
253
    /**
254
     * Reannounce one or more torrents.
255
     *
256
     * @see https://git.io/transmission-rpc-specs Torrent Action Requests.
257
     *
258
     * @param mixed $ids One or more torrent ids, as described in 3.1 of specs.
259
     *
260
     * @return bool
261
     */
262
    public function reannounce($ids = null): bool
263
    {
264
        $this->api('torrent-reannounce', compact('ids'));
265
266
        return true;
267
    }
268
269
    /**
270
     * Set properties of one or more torrents.
271
     *
272
     * @see https://git.io/transmission-rpc-specs "torrent-set" for available arguments.
273
     *
274
     * @param mixed $ids       One or more torrent ids, as described in 3.1 of specs.
275
     * @param array $arguments An associative array of arguments to set.
276
     *
277
     * @return bool
278
     */
279
    public function set($ids, array $arguments): bool
280
    {
281
        $arguments['ids'] = $ids;
282
        $this->api('torrent-set', $arguments);
283
284
        return true;
285
    }
286
287
    /**
288
     * Get information on torrents, if the ids parameter is
289
     * null all torrents will be returned.
290
     *
291
     * @see https://git.io/transmission-rpc-specs "torrent-get" for available fields.
292
     *
293
     * @param mixed $ids    One or more torrent ids, as described in 3.1 of specs.
294
     * @param array $fields An array of return fields, no value will fallback to default fields.
295
     *
296
     * @return Collection
297
     */
298
    public function get($ids = null, array $fields = null): Collection
299
    {
300
        $fields = $fields ?? Torrent::$fields['default'];
301
        $data = $this->api('torrent-get', compact('ids', 'fields'));
302
303
        $torrentsInfo = data_get($data, 'arguments.torrents', 0);
304
305
        if (blank($torrentsInfo)) {
306
            return collect();
307
        }
308
309
        $torrents = collect($torrentsInfo)->mapInto(Torrent::class);
310
311
        return $torrents->count() > 1 ? $torrents : $torrents->first();
312
    }
313
314
    /**
315
     * Add a torrent to the download queue
316
     *
317
     * @see https://git.io/transmission-rpc-specs "torrent-add" for available arguments.
318
     *
319
     * @param  string  $torrent   Filename/URL of the .torrent file OR Magnet URI/.torrent content.
320
     * @param  boolean $metainfo  Is given torrent a metainfo? (default: false).
321
     * @param  string  $savepath  Path to download the torrent to.
322
     * @param  array   $arguments Other optional arguments.
323
     *
324
     * @return Collection
325
     */
326
    public function add(
327
        string $torrent,
328
        bool $metainfo = false,
329
        string $savepath = null,
330
        array $arguments = []
331
    ): Collection {
332
        $arguments[$metainfo ? 'metainfo' : 'filename'] = $metainfo ? base64_encode($torrent) : $torrent;
333
334
        if ($savepath !== null) {
335
            $arguments['download-dir'] = (string)$savepath;
336
        }
337
338
        $data = $this->api('torrent-add', $arguments);
339
340
        if (array_key_exists('torrent-duplicate', $data['arguments'])) {
341
            return $this->get($data['arguments']['torrent-duplicate']['id']);
342
        }
343
344
        if (!array_key_exists('torrent-added', $data['arguments'])) {
345
            throw new InvalidArgumentException($data['result']);
346
        }
347
348
        return collect($data['arguments']['torrent-added']);
349
    }
350
351
    /**
352
     * Remove one or more torrents.
353
     *
354
     * @see https://git.io/transmission-rpc-specs "torrent-remove" for available arguments.
355
     *
356
     * @param mixed $ids             One or more torrent ids, as described in 3.1 of specs.
357
     * @param bool  $deleteLocalData Also remove local data? (default: false).
358
     *
359
     * @return bool
360
     */
361
    public function remove($ids, bool $deleteLocalData = false): bool
362
    {
363
        $arguments = ['ids' => $ids, 'delete-local-data' => $deleteLocalData];
364
        $this->api('torrent-remove', $arguments);
365
366
        return true;
367
    }
368
369
    /**
370
     * Move one or more torrents to new location.
371
     *
372
     * @see https://git.io/transmission-rpc-specs "torrent-set-location" for available arguments.
373
     *
374
     * @param mixed  $ids      One or more torrent ids, as described in 3.1 of specs.
375
     * @param string $location The new torrent location.
376
     * @param bool   $move     Move from previous location or search "location" for files (default: true).
377
     *
378
     * @return bool
379
     */
380
    public function move($ids, string $location, bool $move = true): bool
381
    {
382
        $this->api('torrent-set-location', compact('ids', 'location', 'move'));
383
384
        return true;
385
    }
386
387
    /**
388
     * Rename a Torrent's Path.
389
     *
390
     * @see https://git.io/transmission-rpc-specs "torrent-rename-path" for available arguments.
391
     *
392
     * @param mixed  $ids  One torrent id, as described in 3.1 of specs.
393
     * @param string $path The path to the file or folder that will be renamed.
394
     * @param string $name The file or folder's new name.
395
     *
396
     * @return array
397
     */
398
    public function rename($ids, string $path, string $name): array
399
    {
400
        return $this->api('torrent-rename-path', compact('ids', 'path', 'name'));
401
    }
402
403
    /**
404
     * Set the transmission settings.
405
     *
406
     * @see https://git.io/transmission-rpc-specs "session-set" for available arguments.
407
     *
408
     * @param array $arguments one or more of spec's arguments, except: "blocklist-size",
409
     *                         "config-dir", "rpc-version", "rpc-version-minimum",
410
     *                         "version", and "session-id"
411
     *
412
     * @return bool
413
     */
414
    public function setSettings(array $arguments): bool
415
    {
416
        $this->api('session-set', $arguments);
417
418
        return true;
419
    }
420
421
    /**
422
     * Get the transmission settings.
423
     *
424
     * @see https://git.io/transmission-rpc-specs "session-get" for available fields.
425
     *
426
     * @param array|null $fields
427
     *
428
     * @return array
429
     */
430
    public function getSettings(array $fields = null): array
431
    {
432
        return $this->api('session-get', compact('fields'));
433
    }
434
435
    /**
436
     * Get Session Stats.
437
     *
438
     * @see https://git.io/transmission-rpc-specs "session-stats" for response arguments.
439
     *
440
     * @return array
441
     */
442
    public function sessionStats(): array
443
    {
444
        return $this->api('session-stats');
445
    }
446
447
    /**
448
     * Trigger Blocklist Update.
449
     *
450
     * @see https://git.io/transmission-rpc-specs "blocklist-update" for response arguments.
451
     *
452
     * @return array
453
     */
454
    public function updateBlocklist(): array
455
    {
456
        return $this->api('blocklist-update');
457
    }
458
459
    /**
460
     * Port Test: See if your incoming peer port is accessible from the outside world.
461
     *
462
     * @see https://git.io/transmission-rpc-specs "port-test" for response arguments.
463
     *
464
     * @return bool
465
     */
466
    public function portTest(): bool
467
    {
468
        return $this->api('port-test')['arguments']['port-is-open'];
469
    }
470
471
    /**
472
     * Shutdown Transmission.
473
     *
474
     * @see https://git.io/transmission-rpc-specs "session-close".
475
     *
476
     * @return bool
477
     */
478
    public function close(): bool
479
    {
480
        $this->api('session-close');
481
482
        return true;
483
    }
484
485
    /**
486
     * Move one or more torrents to top in queue.
487
     *
488
     * @see https://git.io/transmission-rpc-specs Queue Movement Requests.
489
     *
490
     * @param mixed $ids One or more torrent ids, sha1 hash strings, or both OR "recently-active", for recently-active
491
     *                   torrents. All torrents are used if no value is given.
492
     *
493
     * @return bool
494
     */
495
    public function queueMoveTop($ids = null): bool
496
    {
497
        $this->api('queue-move-top', compact('ids'));
498
499
        return true;
500
    }
501
502
    /**
503
     * Move one or more torrents up in queue.
504
     *
505
     * @see https://git.io/transmission-rpc-specs Queue Movement Requests.
506
     *
507
     * @param mixed $ids One or more torrent ids, sha1 hash strings, or both OR "recently-active", for recently-active
508
     *                   torrents. All torrents are used if no value is given.
509
     *
510
     * @return bool
511
     */
512
    public function queueMoveUp($ids = null): bool
513
    {
514
        $this->api('queue-move-top', compact('ids'));
515
516
        return true;
517
    }
518
519
    /**
520
     * Move one or more torrents down in queue.
521
     *
522
     * @see https://git.io/transmission-rpc-specs Queue Movement Requests.
523
     *
524
     * @param mixed $ids One or more torrent ids, sha1 hash strings, or both OR "recently-active", for recently-active
525
     *                   torrents. All torrents are used if no value is given.
526
     *
527
     * @return bool
528
     */
529
    public function queueMoveDown($ids = null): bool
530
    {
531
        $this->api('queue-move-down', compact('ids'));
532
533
        return true;
534
    }
535
536
    /**
537
     * Move one or more torrents to bottom in queue.
538
     *
539
     * @see https://git.io/transmission-rpc-specs Queue Movement Requests.
540
     *
541
     * @param mixed $ids One or more torrent ids, sha1 hash strings, or both OR "recently-active", for recently-active
542
     *                   torrents. All torrents are used if no value is given.
543
     *
544
     * @return bool
545
     */
546
    public function queueMoveBottom($ids = null): bool
547
    {
548
        $this->api('queue-move-bottom', compact('ids'));
549
550
        return true;
551
    }
552
553
    /**
554
     * Free Space: Tests how much free space is available in a client-specified folder.
555
     *
556
     * @see https://git.io/transmission-rpc-specs "free-space" for arguments.
557
     *
558
     * @param null|string $path Path to check free space (default: download-dir).
559
     *
560
     * @return array
561
     */
562
    public function freeSpace(string $path = null): array
563
    {
564
        if (blank($path)) {
565
            $path = $this->getSettings()['arguments']['download-dir'];
566
        }
567
568
        return $this->api('free-space', compact('path'))['arguments'];
569
    }
570
571
    /**
572
     * Seed Ratio Limit.
573
     *
574
     * @return mixed
575
     */
576
    public function seedRatioLimit()
577
    {
578
        $settings = $this->getSettings(['seedRatioLimited', 'seedRatioLimit'])['arguments'];
579
580
        if (isset($settings['seedRatioLimited'])) {
581
            return $settings['seedRatioLimit'];
582
        }
583
584
        return -1;
585
    }
586
587
    /**
588
     * Update Download Dir.
589
     *
590
     * @param string $downloadDir Path to download torrents.
591
     *
592
     * @return bool
593
     */
594
    public function updateDownloadDir(string $downloadDir): bool
595
    {
596
        $settings = [
597
            'download-dir' => $downloadDir,
598
        ];
599
600
        return $this->setSettings($settings);
601
    }
602
603
    /**
604
     * Update & Enable Incomplete Dir.
605
     *
606
     * @param string $incompleteDir       Path to store incomplete torrents.
607
     * @param bool   $enableIncompleteDir Is incomplete dir enabled? (default: true).
608
     *
609
     * @return bool
610
     */
611
    public function updateIncompleteDir(string $incompleteDir, bool $enableIncompleteDir = true): bool
612
    {
613
        $settings = [
614
            'incomplete-dir-enabled' => $enableIncompleteDir,
615
            'incomplete-dir'         => $incompleteDir,
616
        ];
617
618
        return $this->setSettings($settings);
619
    }
620
621
    /**
622
     * Request API.
623
     *
624
     * @param string $method
625
     * @param array  $params
626
     *
627
     * @return mixed
628
     */
629
    protected function api(string $method, array $params = [])
630
    {
631
        $arguments = ParamBuilder::build($params);
632
633
        $body = json_encode(compact('method', 'arguments'));
634
635
        $response = $this->getHttpClient()
636
            ->post(
637
                $this->transmissionUrl(),
638
                ['Content-Type' => 'application/json'],
639
                $body
640
            );
641
642
        if (ResponseMediator::isConflictError($response)) {
643
            $this->findAndSetSessionId($response);
644
645
            return $this->api($method, $params);
646
        }
647
648
        return ResponseMediator::getContent($response);
649
    }
650
651
    /**
652
     * Find and Set Session ID from the response.
653
     *
654
     * @param ResponseInterface $response
655
     *
656
     * @return Client
657
     * @throws TransmissionException
658
     */
659
    protected function findAndSetSessionId(ResponseInterface $response): self
660
    {
661
        $sessionId = $response->getHeaderLine('x-transmission-session-id');
662
663
        if (blank($sessionId)) {
664
            throw new TransmissionException('Unable to retrieve X-Transmission-Session-Id');
665
        }
666
667
        $this->setSessionId($sessionId);
668
669
        return $this;
670
    }
671
672
    /**
673
     * Transmission-RPC API URL.
674
     *
675
     * @return string
676
     */
677
    protected function transmissionUrl(): string
678
    {
679
        return $this->hostname . ':' . $this->port . $this->path;
680
    }
681
682
    /**
683
     * Default User Agent for all HTTP Requests.
684
     *
685
     * @return string HTTP User Agent.
686
     */
687
    protected function defaultUserAgent(): string
688
    {
689
        return 'PHP-Transmission-SDK/' . self::VERSION;
690
    }
691
692
    /**
693
     * Get HTTP Client.
694
     *
695
     * @return HttpMethodsClient
696
     */
697
    public function getHttpClient(): HttpMethodsClient
698
    {
699
        return $this->httpClientBuilder->getHttpClient();
700
    }
701
702
    /**
703
     * @return History
704
     */
705
    public function getResponseHistory()
706
    {
707
        return $this->responseHistory;
708
    }
709
710
    /**
711
     * @param $method
712
     * @param $arguments
713
     *
714
     * @return mixed
715
     */
716
    public function __call($method, $arguments)
717
    {
718
        throw new \BadMethodCallException(sprintf(
719
            'Method %s::%s does not exist.', static::class, $method
720
        ));
721
    }
722
}