Passed
Push — master ( bd10da...d5e00b )
by Irfaq
14:58
created

Client::add()   A

Complexity

Conditions 6
Paths 24

Size

Total Lines 25
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 11
dl 0
loc 25
rs 9.2222
c 0
b 0
f 0
cc 6
nc 24
nop 4
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.2.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');
83
        $this->port = $port ?? env('TRANSMISSION_PORT', 9091);
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);
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 All Torrents.
190
     *
191
     * @see start()
192
     *
193
     * @return bool
194
     */
195
    public function startAll(): bool
196
    {
197
        return $this->start();
198
    }
199
200
    /**
201
     * Start one or more torrents.
202
     *
203
     * @see https://git.io/transmission-rpc-specs Torrent Action Requests.
204
     *
205
     * @param mixed $ids One or more torrent ids, sha1 hash strings, or both OR "recently-active", for recently-active
206
     *                   torrents. All torrents are used if no value is given.
207
     *
208
     * @return bool
209
     */
210
    public function start($ids = null): bool
211
    {
212
        $this->api('torrent-start', compact('ids'));
213
214
        return true;
215
    }
216
217
    /**
218
     * Start Now one or more torrents.
219
     *
220
     * @see https://git.io/transmission-rpc-specs Torrent Action Requests.
221
     *
222
     * @param mixed $ids One or more torrent ids, as described in 3.1 of specs.
223
     *
224
     * @return bool
225
     */
226
    public function startNow($ids = null): bool
227
    {
228
        $this->api('torrent-start-now', compact('ids'));
229
230
        return true;
231
    }
232
233
    /**
234
     * Stop All Torrents.
235
     *
236
     * @see stop()
237
     *
238
     * @return bool
239
     */
240
    public function stopAll()
241
    {
242
        return $this->stop();
243
    }
244
245
    /**
246
     * Stop one or more torrents.
247
     *
248
     * @see https://git.io/transmission-rpc-specs Torrent Action Requests.
249
     *
250
     * @param mixed $ids One or more torrent ids, as described in 3.1 of specs.
251
     *
252
     * @return bool
253
     */
254
    public function stop($ids = null): bool
255
    {
256
        $this->api('torrent-stop', compact('ids'));
257
258
        return true;
259
    }
260
261
    /**
262
     * Verify one or more torrents.
263
     *
264
     * @see https://git.io/transmission-rpc-specs Torrent Action Requests.
265
     *
266
     * @param mixed $ids One or more torrent ids, as described in 3.1 of specs.
267
     *
268
     * @return bool
269
     */
270
    public function verify($ids = null): bool
271
    {
272
        $this->api('torrent-verify', compact('ids'));
273
274
        return true;
275
    }
276
277
    /**
278
     * Reannounce one or more torrents.
279
     *
280
     * @see https://git.io/transmission-rpc-specs Torrent Action Requests.
281
     *
282
     * @param mixed $ids One or more torrent ids, as described in 3.1 of specs.
283
     *
284
     * @return bool
285
     */
286
    public function reannounce($ids = null): bool
287
    {
288
        $this->api('torrent-reannounce', compact('ids'));
289
290
        return true;
291
    }
292
293
    /**
294
     * Set properties of one or more torrents.
295
     *
296
     * @see https://git.io/transmission-rpc-specs "torrent-set" for available arguments.
297
     *
298
     * @param mixed $ids       One or more torrent ids, as described in 3.1 of specs.
299
     * @param array $arguments An associative array of arguments to set.
300
     *
301
     * @return bool
302
     */
303
    public function set($ids, array $arguments): bool
304
    {
305
        $arguments['ids'] = $ids;
306
        $this->api('torrent-set', $arguments);
307
308
        return true;
309
    }
310
311
    /**
312
     * Get All Torrents.
313
     *
314
     * @param array|null $fields
315
     *
316
     * @return Collection
317
     */
318
    public function getAll(array $fields = null): Collection
319
    {
320
        return $this->get(null, $fields);
321
    }
322
323
    /**
324
     * Get information on torrents, if the ids parameter is
325
     * null all torrents will be returned.
326
     *
327
     * @see https://git.io/transmission-rpc-specs "torrent-get" for available fields.
328
     *
329
     * @param mixed $ids    One or more torrent ids, as described in 3.1 of specs.
330
     * @param array $fields An array of return fields, no value will fallback to default fields.
331
     *
332
     * @return Collection
333
     */
334
    public function get($ids = null, array $fields = null): Collection
335
    {
336
        $fields = $fields ?? Torrent::$fields['default'];
337
        $data = $this->api('torrent-get', compact('ids', 'fields'));
338
339
        $torrentsInfo = data_get($data, 'arguments.torrents', 0);
340
341
        if (blank($torrentsInfo)) {
342
            return collect();
343
        }
344
345
        $torrents = collect($torrentsInfo)->mapInto(Torrent::class);
346
347
        return $torrents;
348
    }
349
350
    /**
351
     * Add a Torrent File to the download queue.
352
     *
353
     * @param string      $file         Torrent File Content.
354
     * @param string|null $savepath     Path to download the torrent to.
355
     * @param array       $optionalArgs Other optional arguments.
356
     *
357
     * @return Collection
358
     */
359
    public function addFile($file, string $savepath = null, array $optionalArgs = [])
360
    {
361
        return $this->add($file, true, $savepath, $optionalArgs);
362
    }
363
364
    /**
365
     * Add a Torrent by URL to the download queue.
366
     *
367
     * @param string      $url          Magnet URI/URL of the torrent file.
368
     * @param string|null $savepath     Path to download the torrent to.
369
     * @param array       $optionalArgs Other optional arguments.
370
     *
371
     * @return Collection
372
     */
373
    public function addUrl($url, string $savepath = null, array $optionalArgs = [])
374
    {
375
        return $this->add($url, false, $savepath, $optionalArgs);
376
    }
377
378
    /**
379
     * Add a torrent to the download queue
380
     *
381
     * @see https://git.io/transmission-rpc-specs "torrent-add" for available arguments.
382
     *
383
     * @param  string  $torrent      Magnet URI/URL of the torrent file OR .torrent content.
384
     * @param  boolean $metainfo     Is given torrent a metainfo? (default: false).
385
     * @param  string  $savepath     Path to download the torrent to.
386
     * @param  array   $optionalArgs Other optional arguments.
387
     *
388
     * @return Collection
389
     */
390
    public function add(
391
        string $torrent,
392
        bool $metainfo = false,
393
        string $savepath = null,
394
        array $optionalArgs = []
395
    ): Collection {
396
        $arguments = [];
397
        $arguments['paused'] = false; // To start immediately
398
        $arguments[$metainfo ? 'metainfo' : 'filename'] = $metainfo ? base64_encode($torrent) : $torrent;
399
400
        if ($savepath !== null) {
401
            $arguments['download-dir'] = (string)$savepath;
402
        }
403
404
        $data = $this->api('torrent-add', array_merge($arguments, $optionalArgs));
405
406
        if (array_key_exists('torrent-duplicate', $data['arguments'])) {
407
            return collect($data['arguments']['torrent-duplicate']);
408
        }
409
410
        if (!array_key_exists('torrent-added', $data['arguments'])) {
411
            throw new InvalidArgumentException($data['result']);
412
        }
413
414
        return collect($data['arguments']['torrent-added']);
415
    }
416
417
    /**
418
     * Remove one or more torrents.
419
     *
420
     * @see https://git.io/transmission-rpc-specs "torrent-remove" for available arguments.
421
     *
422
     * @param mixed $ids             One or more torrent ids, as described in 3.1 of specs.
423
     * @param bool  $deleteLocalData Also remove local data? (default: false).
424
     *
425
     * @return bool
426
     */
427
    public function remove($ids, bool $deleteLocalData = false): bool
428
    {
429
        $arguments = ['ids' => $ids, 'delete-local-data' => $deleteLocalData];
430
        $this->api('torrent-remove', $arguments);
431
432
        return true;
433
    }
434
435
    /**
436
     * Move one or more torrents to new location.
437
     *
438
     * @see https://git.io/transmission-rpc-specs "torrent-set-location" for available arguments.
439
     *
440
     * @param mixed  $ids      One or more torrent ids, as described in 3.1 of specs.
441
     * @param string $location The new torrent location.
442
     * @param bool   $move     Move from previous location or search "location" for files (default: true).
443
     *
444
     * @return bool
445
     */
446
    public function move($ids, string $location, bool $move = true): bool
447
    {
448
        $this->api('torrent-set-location', compact('ids', 'location', 'move'));
449
450
        return true;
451
    }
452
453
    /**
454
     * Rename a Torrent's Path.
455
     *
456
     * @see https://git.io/transmission-rpc-specs "torrent-rename-path" for available arguments.
457
     *
458
     * @param mixed  $ids  One torrent id, as described in 3.1 of specs.
459
     * @param string $path The path to the file or folder that will be renamed.
460
     * @param string $name The file or folder's new name.
461
     *
462
     * @return array
463
     */
464
    public function rename($ids, string $path, string $name): array
465
    {
466
        return $this->api('torrent-rename-path', compact('ids', 'path', 'name'));
467
    }
468
469
    /**
470
     * Set the transmission settings.
471
     *
472
     * @see https://git.io/transmission-rpc-specs "session-set" for available arguments.
473
     *
474
     * @param array $arguments one or more of spec's arguments, except: "blocklist-size",
475
     *                         "config-dir", "rpc-version", "rpc-version-minimum",
476
     *                         "version", and "session-id"
477
     *
478
     * @return bool
479
     */
480
    public function setSettings(array $arguments): bool
481
    {
482
        $this->api('session-set', $arguments);
483
484
        return true;
485
    }
486
487
    /**
488
     * Get the transmission settings.
489
     *
490
     * @see https://git.io/transmission-rpc-specs "session-get" for available fields.
491
     *
492
     * @param array|null $fields
493
     *
494
     * @return array
495
     */
496
    public function getSettings(array $fields = null): array
497
    {
498
        return $this->api('session-get', compact('fields'));
499
    }
500
501
    /**
502
     * Get Session Stats.
503
     *
504
     * @see https://git.io/transmission-rpc-specs "session-stats" for response arguments.
505
     *
506
     * @return array
507
     */
508
    public function sessionStats(): array
509
    {
510
        return $this->api('session-stats');
511
    }
512
513
    /**
514
     * Trigger Blocklist Update.
515
     *
516
     * @see https://git.io/transmission-rpc-specs "blocklist-update" for response arguments.
517
     *
518
     * @return array
519
     */
520
    public function updateBlocklist(): array
521
    {
522
        return $this->api('blocklist-update');
523
    }
524
525
    /**
526
     * Port Test: See if your incoming peer port is accessible from the outside world.
527
     *
528
     * @see https://git.io/transmission-rpc-specs "port-test" for response arguments.
529
     *
530
     * @return bool
531
     */
532
    public function portTest(): bool
533
    {
534
        return $this->api('port-test')['arguments']['port-is-open'];
535
    }
536
537
    /**
538
     * Shutdown Transmission.
539
     *
540
     * @see https://git.io/transmission-rpc-specs "session-close".
541
     *
542
     * @return bool
543
     */
544
    public function close(): bool
545
    {
546
        $this->api('session-close');
547
548
        return true;
549
    }
550
551
    /**
552
     * Move one or more torrents to top in queue.
553
     *
554
     * @see https://git.io/transmission-rpc-specs Queue Movement Requests.
555
     *
556
     * @param mixed $ids One or more torrent ids, sha1 hash strings, or both OR "recently-active", for recently-active
557
     *                   torrents. All torrents are used if no value is given.
558
     *
559
     * @return bool
560
     */
561
    public function queueMoveTop($ids = null): bool
562
    {
563
        $this->api('queue-move-top', compact('ids'));
564
565
        return true;
566
    }
567
568
    /**
569
     * Move one or more torrents up in queue.
570
     *
571
     * @see https://git.io/transmission-rpc-specs Queue Movement Requests.
572
     *
573
     * @param mixed $ids One or more torrent ids, sha1 hash strings, or both OR "recently-active", for recently-active
574
     *                   torrents. All torrents are used if no value is given.
575
     *
576
     * @return bool
577
     */
578
    public function queueMoveUp($ids = null): bool
579
    {
580
        $this->api('queue-move-top', compact('ids'));
581
582
        return true;
583
    }
584
585
    /**
586
     * Move one or more torrents down in queue.
587
     *
588
     * @see https://git.io/transmission-rpc-specs Queue Movement Requests.
589
     *
590
     * @param mixed $ids One or more torrent ids, sha1 hash strings, or both OR "recently-active", for recently-active
591
     *                   torrents. All torrents are used if no value is given.
592
     *
593
     * @return bool
594
     */
595
    public function queueMoveDown($ids = null): bool
596
    {
597
        $this->api('queue-move-down', compact('ids'));
598
599
        return true;
600
    }
601
602
    /**
603
     * Move one or more torrents to bottom in queue.
604
     *
605
     * @see https://git.io/transmission-rpc-specs Queue Movement Requests.
606
     *
607
     * @param mixed $ids One or more torrent ids, sha1 hash strings, or both OR "recently-active", for recently-active
608
     *                   torrents. All torrents are used if no value is given.
609
     *
610
     * @return bool
611
     */
612
    public function queueMoveBottom($ids = null): bool
613
    {
614
        $this->api('queue-move-bottom', compact('ids'));
615
616
        return true;
617
    }
618
619
    /**
620
     * Free Space: Tests how much free space is available in a client-specified folder.
621
     *
622
     * @see https://git.io/transmission-rpc-specs "free-space" for arguments.
623
     *
624
     * @param null|string $path Path to check free space (default: download-dir).
625
     *
626
     * @return array
627
     */
628
    public function freeSpace(string $path = null): array
629
    {
630
        if (blank($path)) {
631
            $path = $this->getSettings()['arguments']['download-dir'];
632
        }
633
634
        return $this->api('free-space', compact('path'))['arguments'];
635
    }
636
637
    /**
638
     * Seed Ratio Limit.
639
     *
640
     * @return mixed
641
     */
642
    public function seedRatioLimit()
643
    {
644
        $settings = $this->getSettings(['seedRatioLimited', 'seedRatioLimit'])['arguments'];
645
646
        if (isset($settings['seedRatioLimited'])) {
647
            return $settings['seedRatioLimit'];
648
        }
649
650
        return -1;
651
    }
652
653
    /**
654
     * Update Download Dir.
655
     *
656
     * @param string $downloadDir Path to download torrents.
657
     *
658
     * @return bool
659
     */
660
    public function updateDownloadDir(string $downloadDir): bool
661
    {
662
        $settings = [
663
            'download-dir' => $downloadDir,
664
        ];
665
666
        return $this->setSettings($settings);
667
    }
668
669
    /**
670
     * Update & Enable Incomplete Dir.
671
     *
672
     * @param string $incompleteDir       Path to store incomplete torrents.
673
     * @param bool   $enableIncompleteDir Is incomplete dir enabled? (default: true).
674
     *
675
     * @return bool
676
     */
677
    public function updateIncompleteDir(string $incompleteDir, bool $enableIncompleteDir = true): bool
678
    {
679
        $settings = [
680
            'incomplete-dir-enabled' => $enableIncompleteDir,
681
            'incomplete-dir'         => $incompleteDir,
682
        ];
683
684
        return $this->setSettings($settings);
685
    }
686
687
    /**
688
     * Request API.
689
     *
690
     * @param string $method
691
     * @param array  $params
692
     *
693
     * @return mixed
694
     */
695
    protected function api(string $method, array $params = [])
696
    {
697
        $arguments = ParamBuilder::build($params);
698
699
        $body = json_encode(compact('method', 'arguments'));
700
701
        $response = $this->getHttpClient()
702
            ->post(
703
                $this->transmissionUrl(),
704
                ['Content-Type' => 'application/json'],
705
                $body
706
            );
707
708
        if (ResponseMediator::isConflictError($response)) {
709
            $this->findAndSetSessionId($response);
710
711
            return $this->api($method, $params);
712
        }
713
714
        return ResponseMediator::getContent($response);
715
    }
716
717
    /**
718
     * Find and Set Session ID from the response.
719
     *
720
     * @param ResponseInterface $response
721
     *
722
     * @return Client
723
     * @throws TransmissionException
724
     */
725
    protected function findAndSetSessionId(ResponseInterface $response): self
726
    {
727
        $sessionId = $response->getHeaderLine('x-transmission-session-id');
728
729
        if (blank($sessionId)) {
730
            throw new TransmissionException('Unable to retrieve X-Transmission-Session-Id');
731
        }
732
733
        $this->setSessionId($sessionId);
734
735
        return $this;
736
    }
737
738
    /**
739
     * Transmission-RPC API URL.
740
     *
741
     * @return string
742
     */
743
    protected function transmissionUrl(): string
744
    {
745
        return $this->hostname . ':' . $this->port . $this->path;
746
    }
747
748
    /**
749
     * Default User Agent for all HTTP Requests.
750
     *
751
     * @return string HTTP User Agent.
752
     */
753
    protected function defaultUserAgent(): string
754
    {
755
        return 'PHP-Transmission-SDK/' . self::VERSION;
756
    }
757
758
    /**
759
     * Get HTTP Client.
760
     *
761
     * @return HttpMethodsClient
762
     */
763
    public function getHttpClient(): HttpMethodsClient
764
    {
765
        return $this->httpClientBuilder->getHttpClient();
766
    }
767
768
    /**
769
     * @return History
770
     */
771
    public function getResponseHistory()
772
    {
773
        return $this->responseHistory;
774
    }
775
776
    /**
777
     * @param $method
778
     * @param $arguments
779
     *
780
     * @return mixed
781
     */
782
    public function __call($method, $arguments)
783
    {
784
        throw new \BadMethodCallException(sprintf(
785
            'Method %s::%s does not exist.', static::class, $method
786
        ));
787
    }
788
}