├── .gitignore
├── LICENSE
├── README.md
├── composer.json
├── docs
├── Album.md
├── Artist.md
├── Documentation.md
├── Duration.md
├── Episode.md
├── Filter.md
├── Location.md
├── Media.md
├── Movie.md
├── PlexApi.md
├── Season.md
├── Section.md
├── Show.md
├── Size.md
├── Tests.md
└── Track.md
├── phpunit.xml
├── src
└── jc21
│ ├── Collections
│ └── ItemCollection.php
│ ├── Iterators
│ └── ItemIterator.php
│ ├── Movies
│ └── Movie.php
│ ├── Music
│ ├── Album.php
│ ├── Artist.php
│ └── Track.php
│ ├── PlexApi.php
│ ├── Section.php
│ ├── TV
│ ├── Episode.php
│ ├── Season.php
│ └── Show.php
│ └── Util
│ ├── Duration.php
│ ├── Filter.php
│ ├── Item.php
│ ├── Location.php
│ ├── Media.php
│ └── Size.php
└── tests
├── ItemList.php
├── PlexApiTest.php
├── SectionList.php
├── bootstrap.php
└── refresh-library.php
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .idea
3 | vendor
4 | composer.lock
5 | *.env
6 | *.cache
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | 1-clause BSD License (BSD-1-Clause)
2 |
3 | Copyright (c) 2015 jc21
4 |
5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
6 |
7 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
8 |
9 | THIS SOFTWARE IS PROVIDED BY jc21 “AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL jc21 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Plex API for PHP
2 | ================================================
3 |
4 | This is a basic API wrapper for Plex. See the
5 | [documentation](docs/Documentation.md) for functionality.
6 |
7 | This doesn't use the Plex.tv API apart from
8 | signing in.
9 |
10 | XML data returned is interpreted into arrays.
11 |
12 | ### Installing via Composer
13 |
14 | ```bash
15 | # Install Composer
16 | curl -sS https://getcomposer.org/installer | php
17 | ```
18 |
19 | Next, run the Composer command to install the latest stable version:
20 |
21 | ```bash
22 | composer.phar require jc21/plex-api
23 | ```
24 |
25 | After installing, you need to require Composer's autoloader:
26 |
27 | ```php
28 | require 'vendor/autoload.php';
29 | ```
30 |
31 | ### Using
32 |
33 | ```php
34 | use jc21\PlexApi;
35 |
36 | $client = new PlexApi('192.168.0.10');
37 | $client->setAuth('username', 'password');
38 | $result = $client->getOnDeck();
39 | print_r($result);
40 | ```
41 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jc21/plex-api",
3 | "description": "PHP Api for Plex",
4 | "keywords": [
5 | "plex",
6 | "api",
7 | "media"
8 | ],
9 | "homepage": "https://github.com/jc21/plex-api",
10 | "license": "BSD-1-Clause",
11 | "authors": [
12 | {
13 | "name": "Jamie Curnow",
14 | "email": "jc@jc21.com"
15 | }
16 | ],
17 | "minimum-stability": "stable",
18 | "require": {
19 | "php": ">=7.4",
20 | "ext-curl": "*"
21 | },
22 | "require-dev": {
23 | "symfony/dotenv": "6.2.x-dev",
24 | "phpunit/phpunit": "^9"
25 | },
26 | "autoload": {
27 | "psr-0": {
28 | "jc21": "src"
29 | }
30 | },
31 | "scripts": {
32 | "test": "php vendor/bin/phpunit",
33 | "sections": "php tests/SectionList.php",
34 | "items": "php tests/ItemList.php"
35 | }
36 | }
--------------------------------------------------------------------------------
/docs/Album.md:
--------------------------------------------------------------------------------
1 | # Album
2 |
3 | The object to represent a music album
4 |
5 | ## Property List
6 |
7 | | Data type | Property | Description |
8 | | :-------- | :---------------------- | :-------------------------------------------------- |
9 | | int | ratingKey | |
10 | | int | parentRatingKey | |
11 | | string | key | The key to get the details of the album |
12 | | string | parentKey | The link back to the artist |
13 | | string | guid | |
14 | | string | parentGuid | |
15 | | string | studio | The studio that produced the album |
16 | | string | type | The media type `album` |
17 | | string | title | The title of the album |
18 | | string | titleSort | The title used in sorting the album in the UI |
19 | | string | parentTitle | The name of the parent artist |
20 | | string | summary | |
21 | | string | rating | User rating |
22 | | int | index | |
23 | | int | viewCount | |
24 | | int | skipCount | |
25 | | int | year | The year the album was released |
26 | | DateTime | lastVeiwedAt | Date/time the album was last played |
27 | | DateTime | originallyAvailableAt | The date/time the album was released |
28 | | DateTime | addedAt | Date/time the album was added to the library |
29 | | DateTime | updatedAt | Date/time the album database entry was last changed |
30 | | string | thumb | URL to thumbnail |
31 | | string | parentThumb | URL to artist thumbnail |
32 | | int | loudnessAnalysisVersion | |
33 | | array | directory | |
34 | | array | genre | Genre's of music on the album |
35 |
36 | ## Function List
37 |
38 | | Visibility | Function (parameters,...): return |
39 | | :------------ | :--------------------------------------------------------------------------------------------------------------------------------------- |
40 | | public | __construct(): void
|
41 | | public | __get(string $var): mixed
Magic getter |
42 | | public | __set(string \$var, mixed $val): void
Magic setter |
43 | | public | getChildren(): ItemCollection:Track
Method to retrieve collection of tracks on this album |
44 | | public | addTrack(Track $a): void |
45 | | public static | fromLibrary(array $library): Album
Create a Album from the Plex API call return |
46 |
--------------------------------------------------------------------------------
/docs/Artist.md:
--------------------------------------------------------------------------------
1 | # Artist
2 |
3 | The object to represent a music artist
4 |
5 | ## Property List
6 |
7 | | Data type | Property | Description |
8 | | :-------- | :----------- | :-------------------------------------------------- |
9 | | int | ratingKey | |
10 | | string | key | The key to get the details of the artist |
11 | | string | guid | |
12 | | string | type | The media type `artist` |
13 | | string | title | The artist's name |
14 | | string | summary | |
15 | | int | index | |
16 | | int | viewCount | Number of times the artist details have been viewed |
17 | | int | skipCount | |
18 | | DateTime | lastViewedAt | Date/time somebody viewed this artist |
19 | | DateTime | addedAt | Date/time this artist was added to the database |
20 | | DateTime | updatedAt | Date/time this artist's database entry was updated |
21 | | string | thumb | URL to thumbnail image |
22 | | string | art | |
23 | | array | genre | Genre's of music the artist has performed in |
24 | | array | country | Country's the albums were recorded in |
25 |
26 | ## Function List
27 | | Visibility | Function (parameters,...): return |
28 | | :------------ | :----------------------------------------------------------------------------------------------------------------------------------------- |
29 | | public | __construct(): void
|
30 | | public | __get(string $var): mixed
Magic getter |
31 | | public | __set(string \$var, mixed $val): void
Magic setter |
32 | | public | getChildren(): ItemCollection:Album
Method to retrieve all albums written by this artist |
33 | | public | addAlbum(Album $a): void |
34 | | public static | fromLibrary(array $library): Artist
Create a Artist from the Plex API call return |
35 |
--------------------------------------------------------------------------------
/docs/Documentation.md:
--------------------------------------------------------------------------------
1 | # Documentation
2 |
3 | ### Main Class
4 |
5 | [PlexApi](PlexApi.md)
6 |
7 | ### Collection Classes
8 |
9 | `ItemCollection` - enables collection functionality and implements the `\IteratorAggregate` interface
10 | `ItemIterator` - enables iterator functionality and implements the `\Iterator` interface
11 |
12 | ### Media Classes
13 |
14 | - Movie
15 | - [Movie](Movie.md)
16 | - TV
17 | - [Show](Show.md)
18 | - [Season](Season.md)
19 | - [Episode](Episode.md)
20 | - Music
21 | - [Artist](Artist.md)
22 | - [Album](Album.md)
23 | - [Track](Track.md)
24 |
25 | ### Utility Classes
26 |
27 | [Duration](Duration.md)
28 | [Filter](Filter.md)
29 | Item - only present for inheritance and `ItemCollection`
30 | [Location](Location.md)
31 | [Media](Media.md)
32 | [Size](Size.md)
33 | [Section](Section.md)
34 |
35 | ### Dev Testing
36 |
37 | [Tests](Tests.md)
38 |
39 |
24 | * setAuth('username', 'password');
27 | * $sections = $client->getLibrarySections();
28 | *
29 | *
30 | */
31 |
32 | class PlexApi
33 | {
34 | public const VERSION = '2.1';
35 |
36 | const GET = 'GET';
37 | const POST = 'POST';
38 | const PUT = 'PUT';
39 | const DELETE = 'DELETE';
40 |
41 | // Plex agents
42 | public const PLEX_AGENT_NONE = 'com.plexapp.agents.none';
43 | public const PLEX_MOVIE_AGENT = 'tv.plex.agents.movie';
44 | public const PLEX_IMDB_MOVIE_AGENT = 'com.plexapp.agents.imdb';
45 | public const PLEX_TV_AGENT = 'tv.plex.agents.series';
46 | public const PLEX_MUSIC_AGENT = 'tv.plex.agents.music';
47 |
48 | // Plex scanners
49 | public const PLEX_MOVIE_SCANNER = 'Plex Movie';
50 | public const PLEX_TV_SCANNER = 'Plex TV Series';
51 | public const PLEX_MUSIC_SCANNER = 'Plex Music';
52 | public const PLEX_PHOTO_SCANNER = 'Plex Photo Scanner';
53 | public const PLEX_VIDEO_SCANNER = 'Plex Video Files Scanner';
54 |
55 | /**
56 | * The hostname/ip of the Plex server
57 | *
58 | * @var string
59 | */
60 | protected $host = null;
61 |
62 | /**
63 | * The Port of the Plex Server
64 | *
65 | * @var int
66 | */
67 | protected $port = 32400;
68 |
69 | /**
70 | * Use SSL communicating with Plex server
71 | *
72 | * @var int
73 | */
74 | protected $ssl = false;
75 |
76 | /**
77 | * Your Plex.tv Username or Email
78 | *
79 | * @var string
80 | */
81 | protected $username = null;
82 |
83 | /**
84 | * Your Plex.tv Password
85 | *
86 | * @var string
87 | */
88 | protected $password = null;
89 |
90 | /**
91 | * The Plex client identifier for this App/Script.
92 | * This shows up in the Devices section of Plex.
93 | *
94 | * @var string
95 | */
96 | protected $clientIdentifier = 'ec87b5d1-b5e4-4114-ad66-19c747d87c1f';
97 |
98 | /**
99 | * The Plex product name.
100 | * This shows up in the Devices section of Plex.
101 | *
102 | * @var string
103 | */
104 | protected $productName = 'PHPClient';
105 |
106 | /**
107 | * The Plex device.
108 | * This shows up in the Devices section of Plex.
109 | *
110 | * @var string
111 | */
112 | protected $device = 'Script';
113 |
114 | /**
115 | * The Plex device name
116 | * This shows up in the Devices section of Plex.
117 | *
118 | * @var string
119 | */
120 | protected $deviceName = 'Script';
121 |
122 | /**
123 | * The Socket Timeout limit in seconds
124 | *
125 | * @var int
126 | *
127 | **/
128 | protected $timeout = 30;
129 |
130 | /**
131 | * The last curl connection stats
132 | *
133 | * @var array
134 | *
135 | **/
136 | protected $lastCallStats = null;
137 |
138 | /**
139 | * The Auth Token obtained from Plex.tv login
140 | *
141 | * @var string
142 | *
143 | **/
144 | protected $token = null;
145 |
146 |
147 | /**
148 | * Instantiate the class with your Host/Port
149 | *
150 | * @param string $host
151 | * @param int $port
152 | */
153 | public function __construct($host = '127.0.0.1', $port = 32400, $ssl = false)
154 | {
155 | $this->host = $host;
156 | $this->port = (int) $port;
157 | $this->ssl = (bool) $ssl;
158 | }
159 |
160 |
161 | /**
162 | * Credentials for logging into Plex.tv.
163 | * Username can also be an email address.
164 | *
165 | * @param string $username
166 | * @param string $password
167 | * @return void
168 | */
169 | public function setAuth($username, $password)
170 | {
171 | $this->username = $username;
172 | $this->password = $password;
173 | }
174 |
175 | /**
176 | * Tests the set username and password and returns the auth token
177 | *
178 | * @return string
179 | */
180 | public function getToken()
181 | {
182 | if ($this->getBaseInfo() !== false) {
183 | return $this->token;
184 | }
185 | return false;
186 | }
187 |
188 | /**
189 | * Sets the token
190 | *
191 | * @return string
192 | */
193 | public function setToken($token)
194 | {
195 | $this->token = $token;
196 | }
197 |
198 | /**
199 | * Get Plex Server basic info
200 | *
201 | * @return array|bool
202 | */
203 | public function getBaseInfo()
204 | {
205 | return $this->call('/');
206 | }
207 |
208 |
209 | /**
210 | * Get Sessions from Plex
211 | *
212 | * @return array|bool
213 | */
214 | public function getSessions()
215 | {
216 | return $this->call('/status/sessions');
217 | }
218 |
219 |
220 | /**
221 | * Get Transcode Sessions from Plex
222 | *
223 | * @return array|bool
224 | */
225 | public function getTranscodeSessions()
226 | {
227 | return $this->call('/transcode/sessions');
228 | }
229 |
230 | /**
231 | * Method to get plex account data
232 | *
233 | * @return array|bool
234 | */
235 | public function getAccount()
236 | {
237 | return $this->call('/myplex/account');
238 | }
239 |
240 | /**
241 | * Get On Deck Info
242 | *
243 | * @param bool $returnCollection
244 | *
245 | * @return ItemCollection|array|bool
246 | */
247 | public function getOnDeck(bool $returnCollection = false)
248 | {
249 | $results = $this->call('/library/onDeck');
250 |
251 | return $this->checkResults($results, $returnCollection);
252 | }
253 |
254 |
255 | /**
256 | * Get Library Sections ie Movies, TV Shows etc
257 | *
258 | * @param bool $returnObjects
259 | *
260 | * @return array|bool
261 | */
262 | public function getLibrarySections(bool $returnObjects = false)
263 | {
264 | $res = $this->call('/library/sections');
265 | if (!$returnObjects): return $res;
266 | endif;
267 |
268 | $ret = [];
269 | foreach ($res['Directory'] as $s) {
270 | $sec = Section::fromLibrary($s);
271 | $ret[] = $sec;
272 | }
273 |
274 | return $ret;
275 | }
276 |
277 | /**
278 | * Get Library Section contents
279 | *
280 | * @param int $sectionKey Obtained using getLibrarySections()
281 | * @param bool $returnCollection
282 | *
283 | * @return ItemCollection|array|bool
284 | */
285 | public function getLibrarySectionContents($sectionKey, bool $returnCollection = false)
286 | {
287 | $results = $this->call('/library/sections/' . $sectionKey . '/all');
288 |
289 | return $this->checkResults($results, $returnCollection);
290 | }
291 |
292 |
293 | /**
294 | * Refresh a Library Section.
295 | * This makes Plex search for new and removed items from the Library paths.
296 | * Doesn't return anything when successful.
297 | *
298 | * @param int $sectionKey Obtained using getLibrarySections()
299 | * @param bool $force
300 | * @return null|bool
301 | */
302 | public function refreshLibrarySection($sectionKey, $force = false)
303 | {
304 | $options = [];
305 | if ($force) {
306 | $options['force'] = 1;
307 | }
308 |
309 | return $this->call('/library/sections/' . $sectionKey . '/refresh', $options);
310 | }
311 |
312 |
313 | /**
314 | * Refresh a specific item.
315 | * Doesn't return anything when successful.
316 | *
317 | * @param int $item
318 | * @param bool $force
319 | * @return null|bool
320 | */
321 | public function refreshMetadata($item, $force = false)
322 | {
323 | $options = [];
324 | if ($force) {
325 | $options['force'] = 1;
326 | }
327 |
328 | return $this->call('/library/metadata/' . (int) $item . '/refresh', $options, self::PUT);
329 | }
330 |
331 |
332 | /**
333 | * Get Recently Added
334 | *
335 | * @param bool $returnCollection
336 | *
337 | * @return ItemCollection|array|bool
338 | */
339 | public function getRecentlyAdded(bool $returnCollection = false)
340 | {
341 | $results = $this->call('/library/recentlyAdded');
342 |
343 | return $this->checkResults($results, $returnCollection);
344 | }
345 |
346 |
347 | /**
348 | * Get Metadata for an Item
349 | *
350 | * @param int $item
351 | * @param bool $returnObject
352 | * @return array|bool|object
353 | */
354 | public function getMetadata($item, bool $returnObject = false)
355 | {
356 | $res = $this->call('/library/metadata/' . (int) $item);
357 | if (!$returnObject): return $res;
358 | endif;
359 |
360 | $tag = (isset($res['Video']) ? 'Video' : null);
361 | $tag = (isset($res['Directory']) ? 'Directory' : $tag);
362 |
363 | $ret = $this->array2object($res[$tag]);
364 | return $ret;
365 | }
366 |
367 |
368 | /**
369 | * Method for getting the artwork for a item
370 | *
371 | * @param Item $i
372 | * @param string $tag
373 | */
374 | public function getArtwork(Item $i, string $tag)
375 | {
376 | return $this->call($i->{$tag}, ['art' => true]);
377 | }
378 |
379 |
380 | /**
381 | * Search for Items
382 | *
383 | * @param string $query
384 | * @param bool $returnCollection
385 | *
386 | * @return ItemCollection|array|bool
387 | */
388 | public function search($query, bool $returnCollection = false)
389 | {
390 | $results = $this->call('/search', ['query' => $query]);
391 |
392 | return $this->checkResults($results, $returnCollection);
393 | }
394 |
395 | /**
396 | * Method to filter a library
397 | *
398 | * @param int $sectionKey
399 | * @param array $filter
400 | * @param bool $returnCollection
401 | *
402 | * @return ItemCollection|array|bool
403 | */
404 | public function filter(int $sectionKey, array $filter, bool $returnCollection = false)
405 | {
406 | $results = $this->call("/library/sections/{$sectionKey}/all", $filter);
407 |
408 | return $this->checkResults($results, $returnCollection);
409 | }
410 |
411 | /**
412 | * Analyze an item
413 | *
414 | * @param int $item
415 | * @return bool
416 | */
417 | public function analyze($item)
418 | {
419 | return $this->call('/library/metadata/' . (int) $item . '/analyze', [], self::PUT);
420 | }
421 |
422 | /**
423 | * Get Potential Metadata Matches for an item
424 | *
425 | * @param int $item
426 | * @param string $agent
427 | * @param string $language
428 | * @return array
429 | */
430 | public function getMatches($item, $agent = 'com.plexapp.agents.imdb', $language = 'en')
431 | {
432 | return $this->call('/library/metadata/' . (int) $item . '/matches', [
433 | 'manual' => 1,
434 | 'agent' => $agent,
435 | 'language' => $language
436 | ], self::GET);
437 | }
438 |
439 | /**
440 | * Set the Metadata Match for an item
441 | *
442 | * @param int $item
443 | * @param string $name
444 | * @param string $guid
445 | * @return array
446 | */
447 | public function setMatch($item, $name, $guid)
448 | {
449 | return $this->call('/library/metadata/' . (int) $item . '/match', [
450 | 'name' => $name,
451 | 'guid' => $guid
452 | ], self::PUT);
453 | }
454 |
455 |
456 | /**
457 | * Get Servers
458 | *
459 | * @return array|bool
460 | */
461 | public function getServers()
462 | {
463 | return $this->call('/servers');
464 | }
465 |
466 |
467 | /**
468 | * setClientIdentifier
469 | *
470 | * @param string $identifier
471 | * @return void
472 | */
473 | public function setClientIdentifier($identifier)
474 | {
475 | $this->clientIdentifier = $identifier;
476 | }
477 |
478 |
479 | /**
480 | * setProductName
481 | *
482 | * @param string $name
483 | * @return void
484 | */
485 | public function setProductName($name)
486 | {
487 | $this->productName = $name;
488 | }
489 |
490 |
491 | /**
492 | * setDevice
493 | *
494 | * @param string $name
495 | * @return void
496 | */
497 | public function setDevice($name)
498 | {
499 | $this->device = $name;
500 | }
501 |
502 |
503 | /**
504 | * setDeviceName
505 | *
506 | * @param string $name
507 | * @return void
508 | */
509 | public function setDeviceName($name)
510 | {
511 | $this->deviceName = $name;
512 | }
513 |
514 |
515 | /**
516 | * setTimeout
517 | *
518 | * @param int $timeout
519 | * @return void
520 | */
521 | public function setTimeout($timeout)
522 | {
523 | $this->timeout = (int) $timeout;
524 | }
525 |
526 |
527 | /**
528 | * Get last curl stats, for debugging purposes
529 | *
530 | * @return array
531 | */
532 | public function getLastCallStats()
533 | {
534 | return $this->lastCallStats;
535 | }
536 |
537 |
538 | /**
539 | * Make an API Call or Login Call
540 | *
541 | * @param string $path
542 | * @param array $params
543 | * @param string $method
544 | * @param bool $isLoginCall
545 | * @return array|bool
546 | * @throws \Exception
547 | */
548 | public function call($path, $params = [], $method = self::GET, $isLoginCall = false)
549 | {
550 | if (!$this->token && !$isLoginCall) {
551 | $this->call('https://plex.tv/users/sign_in.xml', [], self::POST, true);
552 | if (!$this->token) {
553 | return false;
554 | }
555 | }
556 |
557 | if ($isLoginCall) {
558 | $fullUrl = $path;
559 | } else {
560 | $fullUrl = $this->ssl ? 'https://' : 'http://';
561 | $fullUrl .= $this->host . ':' . $this->port . $path;
562 | if ($params && count($params)) {
563 | $fullUrl .= '?' . $this->buildHttpQuery($params);
564 | }
565 | }
566 |
567 | // Setup curl array
568 | $curlOptArray = [
569 | CURLOPT_URL => $fullUrl,
570 | CURLOPT_AUTOREFERER => true,
571 | CURLOPT_RETURNTRANSFER => true,
572 | CURLOPT_CONNECTTIMEOUT => $this->timeout,
573 | CURLOPT_TIMEOUT => $this->timeout,
574 | CURLOPT_FAILONERROR => true,
575 | CURLOPT_FOLLOWLOCATION => true,
576 | CURLOPT_HTTPHEADER => [
577 | 'X-Plex-Client-Identifier: ' . $this->clientIdentifier,
578 | 'X-Plex-Product: ' . $this->productName,
579 | 'X-Plex-Version: ' . self::VERSION,
580 | 'X-Plex-Device: ' . $this->device,
581 | 'X-Plex-Device-Name: ' . $this->deviceName,
582 | 'X-Plex-Platform: Linux',
583 | 'X-Plex-Platform-Version: ' . self::VERSION,
584 | 'X-Plex-Provides: controller',
585 | 'X-Plex-Username: ' . $this->username,
586 | ],
587 | ];
588 |
589 | if ($isLoginCall) {
590 | $curlOptArray[CURLOPT_HTTPAUTH] = CURLAUTH_ANY;
591 | $curlOptArray[CURLOPT_USERPWD] = $this->username . ':' . $this->password;
592 | } else {
593 | $curlOptArray[CURLOPT_HTTPHEADER][] = 'X-Plex-Token: ' . $this->token;
594 | }
595 |
596 | if ($method == self::POST) {
597 | $curlOptArray[CURLOPT_POST] = true;
598 | } elseif ($method != self::GET) {
599 | $curlOptArray[CURLOPT_CUSTOMREQUEST] = $method;
600 | }
601 |
602 | // Reset vars
603 | $this->lastCallStats = null;
604 |
605 | // Send
606 | $resource = curl_init();
607 | curl_setopt_array($resource, $curlOptArray);
608 |
609 | // Send!
610 | $response = curl_exec($resource);
611 |
612 | // Stats
613 | $this->lastCallStats = curl_getinfo($resource);
614 |
615 | // Return if we are getting binary artwork data
616 | if (isset($params['art']) && $params['art'] && $response !== false) {
617 | curl_close($resource);
618 | return $response;
619 | }
620 |
621 | // Errors and redirect failures
622 | if (!$response) {
623 | $response = false;
624 | error_log(curl_errno($resource) . ': ' . curl_error($resource));
625 | } else {
626 | $response = self::xml2array($response);
627 |
628 | if ($isLoginCall) {
629 | if ($this->lastCallStats['http_code'] != 201) {
630 | throw new \Exception(
631 | "Invalid status code in authentication response from Plex.tv, ".
632 | "expected 201 but got {$this->lastCallStats['http_code']}"
633 | );
634 | }
635 |
636 | $this->token = $response['authentication-token'];
637 | }
638 | }
639 |
640 | curl_close($resource);
641 | return $response;
642 | }
643 |
644 | /**
645 | * Method to build a query string
646 | *
647 | * @param array $query
648 | *
649 | * @return string
650 | */
651 | private function buildHttpQuery(array $query): string
652 | {
653 | $ret = '';
654 | $first = reset($query);
655 | if (isset($first) && is_a($first, 'jc21\Util\Filter')) {
656 | foreach ($query as $q) {
657 | $ret .= (string) $q."&";
658 | }
659 | return substr($ret, 0, -1);
660 | }
661 |
662 | return http_build_query($query);
663 | }
664 |
665 | /**
666 | * xml2array
667 | *
668 | * @param $xml
669 | * @return mixed
670 | */
671 | protected static function xml2array($xml)
672 | {
673 | self::normalizeSimpleXML(simplexml_load_string($xml), $result);
674 | return $result;
675 | }
676 |
677 | /**
678 | * Method to convert an array to a collection
679 | *
680 | * @param array $array
681 | *
682 | * @return ItemCollection
683 | */
684 | public static function array2collection($array)
685 | {
686 | if (is_bool($array)) {
687 | return $array;
688 | }
689 |
690 | $ic = new ItemCollection();
691 | if (!isset($array[0])) {
692 | $array[0] = $array;
693 | }
694 |
695 | foreach ($array as $a) {
696 | if (!is_array($a) || !isset($a['type'])) {
697 | continue;
698 | }
699 |
700 | $i = self::array2object($a);
701 |
702 | if (is_null($i)) {
703 | continue;
704 | }
705 |
706 | $ic->addData($i);
707 | }
708 | return $ic;
709 | }
710 |
711 | /**
712 | * Method to convert a returned array from the API to an specific object
713 | *
714 | * @param array $arr
715 | *
716 | * @return Show|Season|Episode|Artist|Album|Track|Movie
717 | */
718 | public static function array2object(array $arr)
719 | {
720 | if (!isset($arr['type'])) {
721 | return null;
722 | }
723 |
724 | $ns = "jc21\\";
725 | if (in_array($arr['type'], ['show', 'season', 'episode'])) {
726 | $ns .= "TV\\";
727 | } elseif (in_array($arr['type'], ['artist', 'album', 'track'])) {
728 | $ns .= "Music\\";
729 | } elseif ($arr['type'] == 'movie') {
730 | $ns .= "Movies\\";
731 | }
732 |
733 | if ($ns == "jc21\\") {
734 | return null;
735 | }
736 |
737 | $class = $ns.ucfirst($arr['type'])."::fromLibrary";
738 | if (!method_exists($ns.ucfirst($arr['type']), "fromLibrary")) {
739 | return null;
740 | }
741 | $ret = $class($arr);
742 | return $ret;
743 | }
744 |
745 | /**
746 | * normalizeSimpleXML
747 | *
748 | * @param $obj
749 | * @param $result
750 | */
751 | protected static function normalizeSimpleXML($obj, &$result)
752 | {
753 | $data = $obj;
754 | if (is_object($data)) {
755 | $data = get_object_vars($data);
756 | }
757 | if (is_array($data)) {
758 | foreach ($data as $key => $value) {
759 | $res = null;
760 | self::normalizeSimpleXML($value, $res);
761 | if (($key == '@attributes') && ($key)) {
762 | $result = $res;
763 | } else {
764 | $result[$key] = $res;
765 | }
766 | }
767 | } else {
768 | $result = $data;
769 | }
770 | }
771 |
772 | /**
773 | * Method to check the results for what is expected
774 | *
775 | * @param array $results
776 | * @param bool $returnCollection
777 | *
778 | * @return ItemCollection|bool|array
779 | */
780 | private function checkResults($results, bool $returnCollection)
781 | {
782 | if (is_bool($results) || !$returnCollection): return $results;
783 | endif;
784 |
785 | $tag = (isset($results['Video']) ? 'Video' : null);
786 | $tag = (isset($results['Directory']) ? 'Directory' : $tag);
787 |
788 | if (is_null($tag)): return false;
789 | endif;
790 |
791 | return $this->array2collection($results[$tag]);
792 | }
793 | }
794 |
--------------------------------------------------------------------------------
/src/jc21/Section.php:
--------------------------------------------------------------------------------
1 | data = [
50 | 'allowSync' => false,
51 | 'art' => null,
52 | 'composite' => null,
53 | 'filters' => false,
54 | 'refreshing' => false,
55 | 'thumb' => null,
56 | 'key' => null,
57 | 'libraryType' => null,
58 | 'type' => null,
59 | 'title' => null,
60 | 'agent' => null,
61 | 'scanner' => null,
62 | 'language' => null,
63 | 'uuid' => null,
64 | 'createdAt' => null,
65 | 'updatedAt' => null,
66 | 'scannedAt' => null,
67 | 'content' => null,
68 | 'directory' => null,
69 | 'contentChangedAt' => null,
70 | 'hidden' => false,
71 | 'location' => new Location(),
72 | ];
73 | }
74 |
75 | /**
76 | * Magic getter method
77 | *
78 | * @param string $var
79 | *
80 | * @return mixed
81 | */
82 | public function __get(string $var): mixed
83 | {
84 | if (isset($this->data[$var])) {
85 | return $this->data[$var];
86 | }
87 |
88 | return null;
89 | }
90 |
91 | /**
92 | * Magic setter method
93 | *
94 | * @param string $var
95 | * @param mixed $val
96 | */
97 | public function __set(string $var, $val)
98 | {
99 | $this->data[$var] = $val;
100 | }
101 |
102 | /**
103 | * Method to create an object from library data
104 | *
105 | * @param array $lib
106 | *
107 | * @return Section
108 | */
109 | public static function fromLibrary(array $lib): self
110 | {
111 | $me = new static();
112 |
113 | $me->data = $lib;
114 | $me->allowSync = (bool) $lib['allowSync'];
115 | $me->hidden = (bool) $lib['hidden'];
116 | $me->filters = (bool) $lib['filters'];
117 | $me->refreshing = (bool) $lib['refreshing'];
118 | $me->location = Location::fromLibrary($lib['Location']);
119 |
120 | unset($me->data['Location']);
121 |
122 | $me->libraryType = 'public';
123 | if ($me->agent == PlexApi::PLEX_AGENT_NONE) {
124 | $me->libraryType = 'personal';
125 | }
126 |
127 | $createdAt = new DateTime();
128 | $createdAt->setTimestamp($lib['createdAt']);
129 | $me->createdAt = $createdAt;
130 |
131 | $updatedAt = new DateTime();
132 | $updatedAt->setTimestamp($lib['updatedAt']);
133 | $me->updatedAt = $updatedAt;
134 |
135 | $scannedAt = new DateTime();
136 | $scannedAt->setTimestamp($lib['scannedAt']);
137 | $me->scannedAt = $scannedAt;
138 |
139 | return $me;
140 | }
141 |
142 | /**
143 | * Method to serialize the object into json
144 | *
145 | * @return mixed
146 | */
147 | public function jsonSerialize(): mixed
148 | {
149 | return $this->data;
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/src/jc21/TV/Episode.php:
--------------------------------------------------------------------------------
1 | data = [
61 | 'ratingKey' => null,
62 | 'parentRatingKey' => null,
63 | 'grandparentRatingKey' => null,
64 | 'key' => null,
65 | 'parentKey' => null,
66 | 'grandparentKey' => null,
67 | 'guid' => null,
68 | 'parentGuid' => null,
69 | 'grandparentGuid' => null,
70 | 'type' => null,
71 | 'title' => null,
72 | 'parentTitle' => null,
73 | 'grandparentTitle' => null,
74 | 'contentRating' => null,
75 | 'summary' => null,
76 | 'index' => null,
77 | 'parentIndex' => null,
78 | 'audienceRating' => null,
79 | 'audienceRatingImage' => null,
80 | 'viewCount' => null,
81 | 'lastViewedAt' => null,
82 | 'thumb' => null,
83 | 'parentThumb' => null,
84 | 'grandparentThumb' => null,
85 | 'art' => null,
86 | 'grandparentArt' => null,
87 | 'duration' => new Duration(0),
88 | 'originallyAvailableAt' => null,
89 | 'addedAt' => null,
90 | 'updatedAt' => null,
91 | 'media' => new Media(),
92 | ];
93 | }
94 |
95 | /**
96 | * Magic getter method
97 | *
98 | * @param string $var
99 | *
100 | * @return mixed
101 | */
102 | public function __get(string $var)
103 | {
104 | if (isset($this->data[$var])) {
105 | return $this->data[$var];
106 | }
107 | return null;
108 | }
109 |
110 | /**
111 | * Magic setter method
112 | *
113 | * @param string $var
114 | * @param mixed $val
115 | */
116 | public function __set(string $var, $val)
117 | {
118 | $this->data[$var] = $val;
119 | }
120 |
121 | /**
122 | * Method to create an object from library data
123 | *
124 | * @param array $lib
125 | *
126 | * @return Episode
127 | */
128 | public static function fromLibrary(array $lib): Episode
129 | {
130 | $me = new static();
131 | $me->data = $lib;
132 |
133 | if (isset($lib['lastViewedAt'])) {
134 | $lastViewedAt = new DateTime();
135 | $lastViewedAt->setTimestamp($lib['lastViewedAt']);
136 | $me->lastViewedAt = $lastViewedAt;
137 | }
138 |
139 | if (isset($lib['addedAt'])) {
140 | $addedAt = new DateTime();
141 | $addedAt->setTimestamp($lib['addedAt']);
142 | $me->addedAt = $addedAt;
143 | }
144 |
145 | if (isset($lib['updatedAt'])) {
146 | $updatedAt = new DateTime();
147 | $updatedAt->setTimestamp($lib['updatedAt']);
148 | $me->updatedAt = $updatedAt;
149 | }
150 |
151 | if (isset($lib['duration'])) {
152 | $me->duration = new Duration($lib['duration']);
153 | }
154 |
155 | if (isset($lib['originallyAvailableAt'])) {
156 | $me->originallyAvailableAt = new DateTime($lib['originallyAvailableAt']);
157 | }
158 |
159 | if (isset($lib['Media']) && $lib['Media'] && count($lib['Media'])) {
160 | $me->media = Media::fromLibrary($lib['Media']);
161 | }
162 |
163 | if (isset($lib['Director']) && is_array($lib['Director'])) {
164 | if (isset($lib['Director']['tag'])) {
165 | $me->data['director'][] = $lib['Director']['tag'];
166 | } else {
167 | foreach ($lib['Director'] as $d) {
168 | $me->data['director'][] = $d['tag'];
169 | }
170 | }
171 | unset($me->data['Director']);
172 | }
173 |
174 | if (isset($lib['Writer']) && is_array($lib['Writer'])) {
175 | if (isset($lib['Writer']['tag'])) {
176 | $me->data['writer'][] = $lib['Writer']['tag'];
177 | } else {
178 | foreach ($lib['Writer'] as $w) {
179 | $me->data['writer'][] = $w['tag'];
180 | }
181 | }
182 | unset($me->data['Writer']);
183 | }
184 |
185 | if (isset($lib['Role']) && is_array($lib['Role'])) {
186 | if (isset($lib['Role']['tag'])) {
187 | $me->data['role'][] = $lib['Role']['tag'];
188 | } else {
189 | foreach ($lib['Role'] as $r) {
190 | $me->data['role'][] = $r['tag'];
191 | }
192 | }
193 | unset($me->data['Role']);
194 | }
195 |
196 | unset($me->data['Media']);
197 |
198 | return $me;
199 | }
200 |
201 | /**
202 | * {@inheritDoc}
203 | */
204 | public function jsonSerialize(): mixed
205 | {
206 | return $this->data;
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/src/jc21/TV/Season.php:
--------------------------------------------------------------------------------
1 | data = [
58 | 'ratingKey' => null,
59 | 'key' => null,
60 | 'parentRatingKey' => null,
61 | 'guid' => null,
62 | 'parentGuid' => null,
63 | 'parentStudio' => null,
64 | 'type' => null,
65 | 'title' => null,
66 | 'parentKey' => null,
67 | 'parentTitle' => null,
68 | 'summary' => null,
69 | 'index' => null,
70 | 'parentYear' => null,
71 | 'thumb' => null,
72 | 'art' => null,
73 | 'parentThumb' => null,
74 | 'parentTheme' => null,
75 | 'leafCount' => null,
76 | 'viewedLeafCount' => null,
77 | 'addedAt' => null,
78 | 'updatedAt' => null,
79 | ];
80 | $this->episodes = new ItemCollection();
81 | }
82 |
83 | /**
84 | * Magic getter method
85 | *
86 | * @param string $var
87 | *
88 | * @return mixed
89 | */
90 | public function __get(string $var)
91 | {
92 | if (isset($this->data[$var])) {
93 | return $this->data[$var];
94 | }
95 | return null;
96 | }
97 |
98 | /**
99 | * Magic setter method
100 | *
101 | * @param string $var
102 | * @param mixed $val
103 | */
104 | public function __set(string $var, $val)
105 | {
106 | $this->data[$var] = $val;
107 | }
108 |
109 | /**
110 | * Method to return the episodes
111 | *
112 | * @return ItemCollection
113 | */
114 | public function getChildren(): ItemCollection
115 | {
116 | return $this->episodes;
117 | }
118 |
119 | /**
120 | * Method to add an episode to the season
121 | *
122 | * @param Episode $e
123 | */
124 | public function addEpisode(Episode $e)
125 | {
126 | $this->episodes->addData($e);
127 | }
128 |
129 | /**
130 | * Method to create an object from the library
131 | *
132 | * @param array $lib
133 | *
134 | * @return Season
135 | */
136 | public static function fromLibrary(array $lib)
137 | {
138 | global $client;
139 |
140 | $me = new static();
141 | $me->data = $lib;
142 |
143 | $addedAt = new DateTime();
144 | $addedAt->setTimestamp($lib['addedAt']);
145 | $me->addedAt = $addedAt;
146 |
147 | $updatedAt = new DateTime();
148 | $updatedAt->setTimestamp($lib['updatedAt']);
149 | $me->updatedAt = $updatedAt;
150 |
151 | $res = $client->call($me->key);
152 |
153 | if (isset($res['Video']) && is_array($res['Video']) && count($res['Video'])) {
154 | if (isset($res['Video'][0])) {
155 | foreach ($res['Video'] as $e) {
156 | $episode = Episode::fromLibrary($e);
157 | $me->addEpisode($episode);
158 | }
159 | } else {
160 | $episode = Episode::fromLibrary($res['Video']);
161 | $me->addEpisode($episode);
162 | }
163 | }
164 |
165 | return $me;
166 | }
167 |
168 | /**
169 | * {@inheritDoc}
170 | */
171 | public function jsonSerialize(): mixed
172 | {
173 | return $this->data;
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/src/jc21/TV/Show.php:
--------------------------------------------------------------------------------
1 | data = [
66 | 'ratingKey' => null,
67 | 'key' => null,
68 | 'guid' => null,
69 | 'studio' => null,
70 | 'type' => null,
71 | 'title' => null,
72 | 'titleSort' => null,
73 | 'episodeSort' => null,
74 | 'contentRating' => null,
75 | 'summary' => null,
76 | 'index' => null,
77 | 'audienceRating' => null,
78 | 'viewCount' => null,
79 | 'skipCount' => null,
80 | 'lastViewedAt' => null,
81 | 'addedAt' => null,
82 | 'updatedAt' => null,
83 | 'year' => null,
84 | 'thumb' => null,
85 | 'art' => null,
86 | 'duration' => new Duration(0),
87 | 'originallyAvailableAt' => null,
88 | 'leafCount' => null,
89 | 'viewedLeafCount' => null,
90 | 'childCount' => null,
91 | 'audienceRatingImage' => null,
92 | 'genre' => [],
93 | 'role' => [],
94 | ];
95 | $this->seasons = [];
96 | }
97 |
98 | /**
99 | * Magic getter method
100 | *
101 | * @param string $var
102 | *
103 | * @return mixed
104 | */
105 | public function __get(string $var)
106 | {
107 | if (isset($this->data[$var])) {
108 | return $this->data[$var];
109 | }
110 | return null;
111 | }
112 |
113 | /**
114 | * Magic setter method
115 | *
116 | * @param string $var
117 | * @param mixed $val
118 | */
119 | public function __set(string $var, $val)
120 | {
121 | $this->data[$var] = $val;
122 | }
123 |
124 | /**
125 | * Method to get the seasons
126 | *
127 | * @return array:Season
128 | */
129 | public function getSeasons(): array
130 | {
131 | return $this->seasons;
132 | }
133 |
134 | /**
135 | * Method to add a season to the show
136 | *
137 | * @param Season $s
138 | *
139 | * @return bool
140 | */
141 | public function addSeason(Season $s)
142 | {
143 | if (!$s->index) {
144 | return false;
145 | }
146 |
147 | $this->seasons[$s->index] = $s;
148 | return true;
149 | }
150 |
151 | /**
152 | * Method to create an object from the library
153 | *
154 | * @param array $lib
155 | *
156 | * @return Show
157 | *
158 | * @throws Exception
159 | */
160 | public static function fromLibrary(array $lib): Show
161 | {
162 | if (!isset($GLOBALS['client'])) {
163 | throw new Exception('PlexApi client `$client` not available');
164 | }
165 | global $client;
166 |
167 | $me = new static();
168 | $me->data = $lib;
169 |
170 | if (isset($lib['lastViewedAt'])) {
171 | $lastViewedAt = new DateTime();
172 | $lastViewedAt->setTimestamp($lib['lastViewedAt']);
173 | $me->lastViewedAt = $lastViewedAt;
174 | }
175 |
176 | if (isset($lib['addedAt'])) {
177 | $addedAt = new DateTime();
178 | $addedAt->setTimestamp($lib['addedAt']);
179 | $me->addedAt = $addedAt;
180 | }
181 |
182 | if (isset($lib['updatedAt'])) {
183 | $updatedAt = new DateTime();
184 | $updatedAt->setTimestamp($lib['updatedAt']);
185 | $me->updatedAt = $updatedAt;
186 | }
187 |
188 | if (isset($lib['duration'])) {
189 | $me->duration = new Duration($lib['duration']);
190 | }
191 |
192 | if (isset($lib['originallyAvailableAt'])) {
193 | $me->originallyAvailableAt = new DateTime($lib['originallyAvailableAt']);
194 | }
195 |
196 | if (isset($lib['Genre']) && is_array($lib['Genre'])) {
197 | if (count($lib['Genre']) == 1) {
198 | $me->data['genre'][] = $lib['Genre']['tag'];
199 | } else {
200 | foreach ($lib['Genre'] as $g) {
201 | $me->data['genre'][] = $g['tag'];
202 | }
203 | }
204 | unset($me->data['Genre']);
205 | }
206 |
207 | if (isset($lib['Director']) && is_array($lib['Director'])) {
208 | if (count($lib['Director']) == 1) {
209 | $me->data['director'][] = $lib['Director']['tag'];
210 | } else {
211 | foreach ($lib['Director'] as $d) {
212 | $me->data['director'][] = $d['tag'];
213 | }
214 | }
215 | unset($me->data['Director']);
216 | }
217 |
218 | if (isset($lib['Writer']) && is_array($lib['Writer'])) {
219 | if (count($lib['Writer']) == 1) {
220 | $me->data['writer'][] = $lib['Writer']['tag'];
221 | } else {
222 | foreach ($lib['Writer'] as $w) {
223 | $me->data['writer'][] = $w['tag'];
224 | }
225 | }
226 | unset($me->data['Writer']);
227 | }
228 |
229 | if (isset($lib['Role']) && is_array($lib['Role'])) {
230 | if (count($lib['Role']) == 1) {
231 | $me->data['role'][] = $lib['Role']['tag'];
232 | } else {
233 | foreach ($lib['Role'] as $r) {
234 | $me->data['role'][] = $r['tag'];
235 | }
236 | }
237 | unset($me->data['Role']);
238 | }
239 |
240 | $res = $client->call($me->key);
241 |
242 | if (isset($res['Directory']) && is_array($res['Directory']) && count($res['Directory'])) {
243 | if (isset($res['Directory'][0])) {
244 | foreach ($res['Directory'] as $s) {
245 | if ($s['title'] == 'All episodes') {
246 | continue;
247 | }
248 |
249 | $season = Season::fromLibrary($s);
250 | $me->addSeason($season);
251 | }
252 | } else {
253 | $season = Season::fromLibrary($res['Directory']);
254 | $me->addSeason($season);
255 | }
256 | }
257 |
258 | return $me;
259 | }
260 |
261 | /**
262 | * {@inheritDoc}
263 | */
264 | public function jsonSerialize(): mixed
265 | {
266 | return $this->data;
267 | }
268 | }
269 |
--------------------------------------------------------------------------------
/src/jc21/Util/Duration.php:
--------------------------------------------------------------------------------
1 | duration = $duration;
25 | }
26 |
27 | /**
28 | * The duration represented in mins
29 | *
30 | * @return string
31 | */
32 | public function minutes(): string
33 | {
34 | $min = gmdate("i", (int)$this->duration / 1000);
35 | $hr = gmdate("H", (int)$this->duration / 1000);
36 | return (($hr * 60)+$min);
37 | }
38 |
39 | /**
40 | * The duration represented in seconds
41 | *
42 | * @return string
43 | */
44 | public function seconds(): string
45 | {
46 | return (string)($this->duration / 1000);
47 | }
48 |
49 | /**
50 | * Method to convert the duration to seconds
51 | *
52 | * @return string
53 | */
54 | public function __toString(): string
55 | {
56 | return gmdate("H:i:s", (int)$this->duration / 1000);
57 | }
58 |
59 | /**
60 | * Method to serialize the object
61 | *
62 | * @return mixed
63 | */
64 | public function jsonSerialize()
65 | {
66 | return (string) $this->duration;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/jc21/Util/Filter.php:
--------------------------------------------------------------------------------
1 | =', '<='])) {
43 | throw new Exception("Invalid filter operator");
44 | }
45 |
46 | if (!in_array($field, ['title', 'rating', 'contentRating', 'year', 'studio', 'resolution'])) {
47 | throw new Exception("Invalid filter field");
48 | }
49 |
50 | $this->field = $field;
51 | $this->operator = $operator;
52 | $this->value = $value;
53 | }
54 |
55 | /**
56 | * Method to make the object a string
57 | *
58 | * @return string
59 | */
60 | public function __toString()
61 | {
62 | $val = urlencode($this->value);
63 | return "{$this->field}{$this->operator}{$val}";
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/jc21/Util/Item.php:
--------------------------------------------------------------------------------
1 | data[$var])) {
32 | return $this->data[$var];
33 | }
34 |
35 | return null;
36 | }
37 |
38 | /**
39 | * Method to create a Location from the library data
40 | *
41 | * @param array $location
42 | *
43 | * @return Location
44 | */
45 | public static function fromLibrary(array $location): self
46 | {
47 | $me = new static();
48 |
49 | $me->data = $location;
50 |
51 | return $me;
52 | }
53 |
54 | /**
55 | * Method to serialize the object
56 | *
57 | * @return mixed
58 | */
59 | public function jsonSerialize()
60 | {
61 | return $this->data;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/jc21/Util/Media.php:
--------------------------------------------------------------------------------
1 | data[$var])) {
50 | return $this->data[$var];
51 | }
52 |
53 | return null;
54 | }
55 |
56 | /**
57 | * Magic setter method
58 | *
59 | * @param string $var
60 | * @param mixed $val
61 | */
62 | public function __set(string $var, $val)
63 | {
64 | $this->data[$var] = $val;
65 | }
66 |
67 | /**
68 | * Method to create an object from the library
69 | *
70 | * @param array $library
71 | *
72 | * @return Media
73 | */
74 | public static function fromLibrary(array $library): Media
75 | {
76 | $me = new static();
77 | $me->data = $library;
78 |
79 | if (isset($library['duration'])) {
80 | $me->duration = new Duration($library['duration']);
81 | }
82 |
83 | if (isset($library['Part'])) {
84 | if (isset($library['Part']['size']) && $library['Part']['size'] > 0) {
85 | $me->size = new Size($library['Part']['size']);
86 | }
87 | if (isset($library['Part']['file'])) {
88 | $me->path = $library['Part']['file'];
89 | }
90 | }
91 |
92 | unset($me->data['Part']);
93 |
94 | return $me;
95 | }
96 |
97 | /**
98 | * Method to serialize the object
99 | *
100 | * @return mixed
101 | */
102 | public function jsonSerialize()
103 | {
104 | return $this->data;
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/jc21/Util/Size.php:
--------------------------------------------------------------------------------
1 | size = $size;
25 | }
26 |
27 | /**
28 | * Returns the size in TB
29 | *
30 | * @return string
31 | */
32 | public function TB()
33 | {
34 | return number_format($this->size / 1024 / 1024 / 1024 / 1024, 3);
35 | }
36 |
37 | /**
38 | * Return the size in GB
39 | *
40 | * @return string
41 | */
42 | public function GB()
43 | {
44 | return number_format($this->size / 1024 / 1024 / 1024, 3);
45 | }
46 |
47 | /**
48 | * Return the size in MB
49 | *
50 | * @return string
51 | */
52 | public function MB()
53 | {
54 | return number_format($this->size / 1024 / 1024, 3);
55 | }
56 |
57 | /**
58 | * Return the size in KB
59 | *
60 | * @return string
61 | */
62 | public function KB()
63 | {
64 | return number_format($this->size / 1024, 3);
65 | }
66 |
67 | /**
68 | * Return the size in bytes
69 | *
70 | * @return int
71 | */
72 | public function bytes()
73 | {
74 | return $this->size;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/tests/ItemList.php:
--------------------------------------------------------------------------------
1 | loadEnv('tests/.env');
10 |
11 | $client = new PlexApi($_ENV['PLEX_HOST']);
12 | $client->setToken($_ENV['PLEX_TOKEN']);
13 |
14 | print PHP_EOL."Select one of the ID's below to put in your .env file for the *_ITEM_ID value".PHP_EOL;
15 |
16 | if (isset($_ENV['MOVIE_TESTS']) && (bool) $_ENV['MOVIE_TESTS']) {
17 | print PHP_EOL."List of 10 Movies".PHP_EOL;
18 | $movieCollection = $client->getLibrarySectionContents($_ENV['MOVIE_SECTION_KEY'], true);
19 | for ($x = 0; $x < 10; $x++) {
20 | $movie = $movieCollection->getData($x);
21 | print "{$movie->ratingKey}: {$movie->title}".PHP_EOL;
22 | }
23 | print PHP_EOL;
24 | }
25 |
26 | if (isset($_ENV['TV_TESTS']) && (bool) $_ENV['TV_TESTS']) {
27 | print "List of 10 TV shows".PHP_EOL;
28 | $tvCollection = $client->getLibrarySectionContents($_ENV['TV_SECTION_KEY'], true);
29 | for ($x = 0; $x < 10; $x++) {
30 | $tv = $tvCollection->getData($x);
31 | print "{$tv->ratingKey}: {$tv->title}".PHP_EOL;
32 | }
33 | print PHP_EOL;
34 | }
35 |
--------------------------------------------------------------------------------
/tests/PlexApiTest.php:
--------------------------------------------------------------------------------
1 | runMovieTests = false;
117 | $this->runTVTests = false;
118 | $this->runMusicTests = false;
119 |
120 | $dot = new Dotenv();
121 |
122 | $envfname = __DIR__.'/.env';
123 | if (!file_exists($envfname) || !is_readable($envfname)) {
124 | throw new \InvalidArgumentException(sprintf('%s does not exist or is not readable', $envfname));
125 | }
126 |
127 | $dot->loadEnv($envfname);
128 |
129 | $this->api = null;
130 | if (!$this->envCheck()) {
131 | die;
132 | }
133 |
134 | $this->api = new PlexApi($this->host, $this->port, $this->ssl);
135 | if ($this->token) {
136 | $this->api->setToken($this->token);
137 | } else {
138 | $this->api->setAuth($this->user, $this->password);
139 |
140 | die("Put this token in your .env file {$envfname} as 'PLEX_TOKEN={$this->api->getToken()}' and then you can remove PLEX_USER and PLEX_PASSWORD if you like");
141 | }
142 |
143 | $GLOBALS['client'] = $this->api;
144 | }
145 |
146 | /**
147 | * Helper method to check available environment variables
148 | *
149 | * @return bool
150 | */
151 | private function envCheck()
152 | {
153 | $this->host = (isset($_ENV['PLEX_HOST']) ? $_ENV['PLEX_HOST'] : false);
154 | $this->token = (isset($_ENV['PLEX_TOKEN']) ? $_ENV['PLEX_TOKEN'] : '');
155 | $this->user = (isset($_ENV['PLEX_USER']) ? $_ENV['PLEX_USER'] : false);
156 | $this->password = (isset($_ENV['PLEX_PASSWORD']) ? $_ENV['PLEX_PASSWORD'] : false);
157 | $this->port = (isset($_ENV['PLEX_PORT']) ? $_ENV['PLEX_PORT'] : 32400);
158 | $this->ssl = (isset($_ENV['PLEX_SSL']) ? (bool) $_ENV['PLEX_SSL'] : false);
159 | $ret = true;
160 |
161 | if ($this->host === false) {
162 | print("PLEX_HOST not found in .env file".PHP_EOL);
163 | }
164 |
165 | if (empty($this->token) && ($this->user === false || $this->password === false)) {
166 | print("PLEX_TOKEN not found in .env file".PHP_EOL);
167 | $ret = false;
168 | }
169 |
170 | if (isset($_ENV['MOVIE_TESTS']) && ((bool) $_ENV['MOVIE_TESTS'])) {
171 | $this->runMovieTests = true;
172 |
173 | if (!isset($_ENV['MOVIE_SECTION_KEY']) || !is_numeric($_ENV['MOVIE_SECTION_KEY'])) {
174 | print("MOVIE_SECTION_KEY not found or not INT in .env, populate with ID of library you want to test".PHP_EOL);
175 | $ret = false;
176 | }
177 |
178 | if (!isset($_ENV['MOVIE_ITEM_ID']) || !is_numeric($_ENV['MOVIE_ITEM_ID'])) {
179 | print("MOVIE_ITEM_ID not found or not INT in .env".PHP_EOL);
180 | $ret = false;
181 | }
182 |
183 | if (!isset($_ENV['MOVIE_SEARCH_QUERY'])) {
184 | print("MOVIE_SEARCH_QUERY not found in .env".PHP_EOL);
185 | $ret = false;
186 | }
187 |
188 | if (!isset($_ENV['MOVIE_FILTER_QUERY'])) {
189 | print("MOVIE_FILTER_QUERY not found in .env, MUST be a title filter".PHP_EOL);
190 | $ret = false;
191 | }
192 | }
193 |
194 | if (isset($_ENV['TV_TESTS']) && ((bool) $_ENV['TV_TESTS'])) {
195 | $this->runTVTests = true;
196 |
197 | if (!isset($_ENV['TV_SECTION_KEY']) || !is_numeric($_ENV['TV_SECTION_KEY'])) {
198 | print("TV_SECTION_KEY not found or not INT in .env, populate with ID of library you want to test".PHP_EOL);
199 | $ret = false;
200 | }
201 |
202 | if (!isset($_ENV['TV_ITEM_ID']) || !is_numeric($_ENV['TV_ITEM_ID'])) {
203 | print("TV_ITEM_ID not found or not INT in .env".PHP_EOL);
204 | $ret = false;
205 | }
206 |
207 | if (!isset($_ENV['TV_SEARCH_QUERY'])) {
208 | print("TV_SEARCH_QUERY not found in .env".PHP_EOL);
209 | $ret = false;
210 | }
211 |
212 | if (!isset($_ENV['TV_FILTER_QUERY'])) {
213 | print("TV_FILTER_QUERY not found in .env, MUST be a title filter".PHP_EOL);
214 | $ret = false;
215 | }
216 | }
217 |
218 | if (isset($_ENV['MUSIC_TESTS']) && ((bool) $_ENV['MUSIC_TESTS'])) {
219 | $this->runMusicTests = true;
220 |
221 | if (!isset($_ENV['MUSIC_SECTION_KEY']) || !is_numeric($_ENV['MUSIC_SECTION_KEY'])) {
222 | print("MUSIC_SECTION_KEY not found in .env".PHP_EOL);
223 | $ret = false;
224 | }
225 |
226 | if (!isset($_ENV['MUSIC_SEARCH_QUERY'])) {
227 | print("MUSIC_SEARCH_QUERY not found in .env".PHP_EOL);
228 | $ret = false;
229 | }
230 |
231 | if (!isset($_ENV['MUSIC_FILTER_QUERY'])) {
232 | print("MUSIC_FILTER_QUERY not found in .env MUST be a title filter".PHP_EOL);
233 | $ret = false;
234 | }
235 | }
236 |
237 | return $ret;
238 | }
239 |
240 | public function testConnection()
241 | {
242 | $this->assertTrue(is_a($this->api, "jc21\PlexApi"));
243 | }
244 |
245 | public function testGetBaseInfo()
246 | {
247 | $res = $this->api->getBaseInfo();
248 | $this->assertIsArray($res);
249 | $this->assertArrayHasKey('size', $res);
250 | $this->assertGreaterThan(0, $res['size']);
251 | }
252 |
253 | public function testGetAccount()
254 | {
255 | $res = $this->api->getAccount();
256 | $this->assertIsArray($res);
257 | $this->assertArrayHasKey('signInState', $res);
258 | $this->assertEquals('ok', $res['signInState']);
259 | }
260 |
261 | public function testGetSessions()
262 | {
263 | $this->assertArrayHasKey('size', $this->api->getSessions());
264 | }
265 |
266 | public function testOnDeck()
267 | {
268 | $od = $this->api->getOnDeck();
269 | $this->assertArrayHasKey('size', $od);
270 | }
271 |
272 | public function testOnDeckReturnCollection()
273 | {
274 | $od = $this->api->getOnDeck(true);
275 | $this->assertInstanceOf(ItemCollection::class, $od);
276 | }
277 |
278 | public function testOnDeckHasItems()
279 | {
280 | $od = $this->api->getOnDeck(true);
281 | $this->assertGreaterThan(0, $od->count());
282 | }
283 |
284 | public function testGetRecentlyAdded()
285 | {
286 | $res = $this->api->getRecentlyAdded();
287 | $this->assertIsArray($res);
288 | $this->assertArrayHasKey('size', $res);
289 | $this->assertGreaterThan(0, $res['size']);
290 | }
291 |
292 | public function testGetSections()
293 | {
294 | $sec = $this->api->getLibrarySections();
295 | $this->assertArrayHasKey('size', $sec);
296 | $this->assertGreaterThan(0, $sec['size']);
297 | }
298 |
299 | public function testGetSectionsAsObject()
300 | {
301 | $secs = $this->api->getLibrarySections(true);
302 | $this->assertIsArray($secs);
303 | $this->assertIsObject($secs[0]);
304 | $this->assertInstanceOf(Section::class, $secs[0]);
305 | }
306 |
307 | public function testGetMovieLibrarySectionContents()
308 | {
309 | if (!$this->runMovieTests) {
310 | $this->markTestSkipped(self::MOVIE_OFF_MSG);
311 | return;
312 | }
313 |
314 | $res = $this->api->getLibrarySectionContents($_ENV['MOVIE_SECTION_KEY']);
315 | $this->assertArrayHasKey('size', $res);
316 | $this->assertGreaterThan(0, $res['size']);
317 | }
318 |
319 | public function testGetMovieLibrarySectionContentsAsCollection()
320 | {
321 | if (!$this->runMovieTests) {
322 | $this->markTestSkipped(self::MOVIE_OFF_MSG);
323 | return;
324 | }
325 |
326 | $res = $this->api->getLibrarySectionContents($_ENV['MOVIE_SECTION_KEY'], true);
327 | $this->assertInstanceOf(ItemCollection::class, $res);
328 | $this->assertGreaterThan(0, $res->count());
329 | }
330 |
331 | public function testGetMovieCollectionSize()
332 | {
333 | if (!$this->runMovieTests) {
334 | $this->markTestSkipped(self::MOVIE_OFF_MSG);
335 | return;
336 | }
337 |
338 | $filter = new Filter('title', $_ENV['MOVIE_FILTER_QUERY']);
339 | $res = $this->api->filter($_ENV['MOVIE_SECTION_KEY'], [$filter], true);
340 |
341 | $this->assertEquals($_ENV['MOVIE_FILTER_QUERY_SIZE'], $res->size()->bytes());
342 | }
343 |
344 | public function testGetMetadata()
345 | {
346 | if (!$this->runMovieTests) {
347 | $this->markTestSkipped(self::MOVIE_OFF_MSG);
348 | return;
349 | }
350 |
351 | $res = $this->api->getMetadata($_ENV['MOVIE_ITEM_ID']);
352 | $this->assertIsArray($res);
353 | $this->assertArrayHasKey('size', $res);
354 | $this->assertGreaterThan(0, $res['size']);
355 | }
356 |
357 | public function testMovieSearch()
358 | {
359 | if (!$this->runMovieTests) {
360 | $this->markTestSkipped(self::MOVIE_OFF_MSG);
361 | return;
362 | }
363 |
364 | $res = $this->api->search($_ENV['MOVIE_SEARCH_QUERY']);
365 | $this->assertIsArray($res);
366 | $this->assertArrayHasKey('Video', $res);
367 | $this->assertGreaterThan(0, count($res['Video']));
368 | }
369 |
370 | public function testMovieSearchReturnCollection()
371 | {
372 | if (!$this->runMovieTests) {
373 | $this->markTestSkipped(self::MOVIE_OFF_MSG);
374 | return;
375 | }
376 |
377 | $res = $this->api->search($_ENV['MOVIE_SEARCH_QUERY'], true);
378 | $this->assertInstanceOf(ItemCollection::class, $res);
379 | $this->assertGreaterThan(0, $res->count());
380 | }
381 |
382 | public function testMovieFilterAsFilterArray()
383 | {
384 | if (!$this->runMovieTests) {
385 | $this->markTestSkipped(self::MOVIE_OFF_MSG);
386 | return;
387 | }
388 |
389 | $res = $this->api->filter($_ENV['MOVIE_SECTION_KEY'], ['title' => $_ENV['MOVIE_FILTER_QUERY']]);
390 | $this->assertIsArray($res);
391 | $this->assertArrayHasKey('size', $res);
392 | $this->assertGreaterThan(0, $res['size']);
393 | }
394 |
395 | public function testMovieFilterWithFilterObject()
396 | {
397 | if (!$this->runMovieTests) {
398 | $this->markTestSkipped(self::MOVIE_OFF_MSG);
399 | return;
400 | }
401 |
402 | $filter = new Filter('title', $_ENV['MOVIE_FILTER_QUERY']);
403 | $res = $this->api->filter($_ENV['MOVIE_SECTION_KEY'], [$filter]);
404 | $this->assertIsArray($res);
405 | $this->assertArrayHasKey('size', $res);
406 | $this->assertGreaterThan(0, $res['size']);
407 | }
408 |
409 | public function testMovieFilterReturnCollection()
410 | {
411 | if (!$this->runMovieTests) {
412 | $this->markTestSkipped(self::MOVIE_OFF_MSG);
413 | return;
414 | }
415 |
416 | $res = $this->api->filter($_ENV['MOVIE_SECTION_KEY'], ['title' => $_ENV['MOVIE_FILTER_QUERY']], true);
417 | $this->assertInstanceOf(ItemCollection::class, $res);
418 | $this->assertGreaterThan(0, $res->count());
419 | }
420 |
421 | public function testMovieFilterWithFilterObjectReturnCollection()
422 | {
423 | if (!$this->runMovieTests) {
424 | $this->markTestSkipped(self::MOVIE_OFF_MSG);
425 | return;
426 | }
427 |
428 | $filter = new Filter('title', $_ENV['MOVIE_FILTER_QUERY']);
429 | $res = $this->api->filter($_ENV['MOVIE_SECTION_KEY'], [$filter], true);
430 | $this->assertInstanceOf(ItemCollection::class, $res);
431 | $this->assertGreaterThan(0, $res->count());
432 | }
433 |
434 | public function testMovieGetMatches()
435 | {
436 | if (!$this->runMovieTests) {
437 | $this->markTestSkipped(self::MOVIE_OFF_MSG);
438 | return;
439 | }
440 |
441 | $res = $this->api->getMatches($_ENV['MOVIE_ITEM_ID']);
442 | $this->assertIsArray($res);
443 | $this->assertArrayHasKey('size', $res);
444 | $this->assertGreaterThan(0, $res['size']);
445 | }
446 |
447 | public function testGetTVLibrarySectionContents()
448 | {
449 | if (!$this->runTVTests) {
450 | $this->markTestSkipped(self::TV_OFF_MSG);
451 | return;
452 | }
453 |
454 | $res = $this->api->getLibrarySectionContents($_ENV['TV_SECTION_KEY']);
455 | $this->assertIsArray($res);
456 | $this->assertArrayHasKey('size', $res);
457 | $this->assertGreaterThan(0, $res['size']);
458 | }
459 |
460 | public function testGetTVLibrarySectionContentsReturnCollection()
461 | {
462 | if (!$this->runTVTests) {
463 | $this->markTestSkipped(self::TV_OFF_MSG);
464 | return;
465 | }
466 |
467 | $res = $this->api->getLibrarySectionContents($_ENV['TV_SECTION_KEY'], true);
468 | $this->assertInstanceOf(ItemCollection::class, $res);
469 | $this->assertGreaterThan(0, $res->count());
470 | }
471 |
472 | public function testGetTVItemMetadata()
473 | {
474 | if (!$this->runTVTests) {
475 | $this->markTestSkipped(self::TV_OFF_MSG);
476 | return;
477 | }
478 |
479 | $res = $this->api->getMetadata($_ENV['TV_ITEM_ID']);
480 | $this->assertIsArray($res);
481 | $this->assertArrayHasKey('size', $res);
482 | $this->assertGreaterThan(0, $res['size']);
483 | }
484 |
485 | public function testGetTVItemMetadataAsObject()
486 | {
487 | if (!$this->runTVTests) {
488 | $this->markTestSkipped(self::TV_OFF_MSG);
489 | return;
490 | }
491 |
492 | $res = $this->api->getMetadata($_ENV['TV_ITEM_ID'], true);
493 | $this->assertInstanceOf(Episode::class, $res);
494 | }
495 |
496 | public function testTVSearch()
497 | {
498 | if (!$this->runTVTests) {
499 | $this->markTestSkipped(self::TV_OFF_MSG);
500 | return;
501 | }
502 |
503 | $res = $this->api->search($_ENV['TV_SEARCH_QUERY']);
504 | $this->assertIsArray($res);
505 | $this->assertArrayHasKey('Video', $res);
506 | $this->assertGreaterThan(0, count($res['Video']));
507 | }
508 |
509 | public function testTVFilterWithFilterObjectReturnCollection()
510 | {
511 | if (!$this->runTVTests) {
512 | $this->markTestSkipped(self::TV_OFF_MSG);
513 | return;
514 | }
515 |
516 | $filter = new Filter('title', $_ENV['TV_FILTER_QUERY']);
517 | $res = $this->api->filter($_ENV['TV_SECTION_KEY'], [$filter], true);
518 | $this->assertInstanceOf(ItemCollection::class, $res);
519 | $this->assertGreaterThan(0, $res->count());
520 | }
521 |
522 | public function testGetMusic()
523 | {
524 | if (!$this->runMusicTests) {
525 | $this->markTestSkipped(self::MUSIC_OFF_MSG);
526 | return;
527 | }
528 |
529 | $res = $this->api->getLibrarySectionContents($_ENV['MUSIC_SECTION_KEY'], true);
530 | $this->assertGreaterThan(0, $res->count());
531 | }
532 |
533 | public function testMusicSearch()
534 | {
535 | if (!$this->runMusicTests) {
536 | $this->markTestSkipped(self::MUSIC_OFF_MSG);
537 | return;
538 | }
539 |
540 | $res = $this->api->search($_ENV['MUSIC_SEARCH_QUERY']);
541 | $this->assertIsArray($res);
542 | $this->assertArrayHasKey('Directory', $res);
543 | $this->assertGreaterThan(0, count($res['Directory']));
544 | }
545 |
546 | public function testMusicFilterAsObject()
547 | {
548 | if (!$this->runMusicTests) {
549 | $this->markTestSkipped(self::MUSIC_OFF_MSG);
550 | return;
551 | }
552 |
553 | $filter = new Filter('title', $_ENV['MUSIC_FILTER_QUERY']);
554 | $res = $this->api->filter($_ENV['MUSIC_SECTION_KEY'], [$filter], true);
555 | $this->assertGreaterThan(0, $res->count());
556 | }
557 | }
558 |
--------------------------------------------------------------------------------
/tests/SectionList.php:
--------------------------------------------------------------------------------
1 | loadEnv("tests/.env");
10 |
11 | $client = new PlexApi($_ENV['PLEX_HOST']);
12 | $client->setToken($_ENV['PLEX_TOKEN']);
13 | $result = $client->getLibrarySections();
14 |
15 | foreach ($result['Directory'] as $section) {
16 | // Output
17 | print "{$section['key']}: {$section['title']} (type: {$section['type']})".PHP_EOL;
18 | }
19 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 | setAuth('username', 'password');
12 |
13 | $result = $client->getLibrarySections();
14 |
15 | foreach ($result['Directory'] as $section) {
16 | // Output
17 | print $section['key'] . ': ' . $section['title'] . ' (type: ' . $section['type'] . ')' . PHP_EOL;
18 |
19 | // Refresh
20 | $client->refreshLibrarySection($section['key']);
21 | }
22 |
--------------------------------------------------------------------------------