Completed
Branch master (205409)
by Timothy
02:36
created

Model::deleteListItem()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
/**
3
 * Anime List Client
4
 *
5
 * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
6
 *
7
 * PHP version 7
8
 *
9
 * @package     AnimeListClient
10
 * @author      Timothy J. Warren <[email protected]>
11
 * @copyright   2015 - 2017  Timothy J. Warren
12
 * @license     http://www.opensource.org/licenses/mit-license.html  MIT License
13
 * @version     4.0
14
 * @link        https://github.com/timw4mail/HummingBirdAnimeClient
15
 */
16
17
namespace Aviat\AnimeClient\API\Kitsu;
18
19
use Aviat\AnimeClient\API\CacheTrait;
20
use Aviat\AnimeClient\API\JsonAPI;
21
use Aviat\AnimeClient\API\Kitsu as K;
22
use Aviat\AnimeClient\API\Kitsu\Transformer\{
23
	AnimeTransformer,
24
	AnimeListTransformer,
25
	MangaTransformer,
26
	MangaListTransformer
27
};
28
use Aviat\Ion\Di\ContainerAware;
29
use Aviat\Ion\Json;
30
use GuzzleHttp\Exception\ClientException;
31
32
/**
33
 * Kitsu API Model
34
 */
35
class Model {
36
	use CacheTrait;
37
	use ContainerAware;
38
	use KitsuTrait;
39
40
	/**
41
	 * Class to map anime list items
42
	 * to a common format used by
43
	 * templates
44
	 *
45
	 * @var AnimeListTransformer
46
	 */
47
	protected $animeListTransformer;
48
49
	/**
50
	 * @var AnimeTransformer
51
	 */
52
	protected $animeTransformer;
53
54
	/**
55
	 * @var ListItem
56
	 */
57
	protected $listItem;
58
59
	/**
60
	 * @var MangaTransformer
61
	 */
62
	protected $mangaTransformer;
63
64
	/**
65
	 * @var MangaListTransformer
66
	 */
67
	protected $mangaListTransformer;
68
69
70
	/**
71
	 * Constructor.
72
	 */
73
	public function __construct(ListItem $listItem)
74
	{
75
		// Set up Guzzle trait
76
		$this->init();
77
78
		$this->animeTransformer = new AnimeTransformer();
0 ignored issues
show
Documentation Bug introduced by
It seems like new \Aviat\AnimeClient\A...rmer\AnimeTransformer() of type object<Aviat\AnimeClient...ormer\AnimeTransformer> is incompatible with the declared type object<AnimeTransformer> of property $animeTransformer.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
79
		$this->animeListTransformer = new AnimeListTransformer();
0 ignored issues
show
Documentation Bug introduced by
It seems like new \Aviat\AnimeClient\A...\AnimeListTransformer() of type object<Aviat\AnimeClient...r\AnimeListTransformer> is incompatible with the declared type object<AnimeListTransformer> of property $animeListTransformer.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
80
		$this->listItem = $listItem;
81
		$this->mangaTransformer = new MangaTransformer();
0 ignored issues
show
Documentation Bug introduced by
It seems like new \Aviat\AnimeClient\A...rmer\MangaTransformer() of type object<Aviat\AnimeClient...ormer\MangaTransformer> is incompatible with the declared type object<MangaTransformer> of property $mangaTransformer.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
82
		$this->mangaListTransformer = new MangaListTransformer();
0 ignored issues
show
Documentation Bug introduced by
It seems like new \Aviat\AnimeClient\A...\MangaListTransformer() of type object<Aviat\AnimeClient...r\MangaListTransformer> is incompatible with the declared type object<MangaListTransformer> of property $mangaListTransformer.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
83
	}
84
85
	/**
86
	 * Get the userid for a username from Kitsu
87
	 *
88
	 * @param string $username
0 ignored issues
show
Documentation introduced by
Should the type for parameter $username not be null|string?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
89
	 * @return string
90
	 */
91
	public function getUserIdByUsername(string $username = NULL)
92
	{
93
		if (is_null($username))
94
		{
95
			$username = $this->getUsername();
96
		}
97
98
		$cacheItem = $this->cache->getItem(K::AUTH_USER_ID_KEY);
99
100
		if ( ! $cacheItem->isHit())
101
		{
102
			$data = $this->getRequest('users', [
103
				'query' => [
104
					'filter' => [
105
						'name' => $username
106
					]
107
				]
108
			]);
109
110
			$cacheItem->set($data['data'][0]['id']);
111
			$cacheItem->save();
112
		}
113
114
		return $cacheItem->get();
115
	}
116
117
	/**
118
	 * Get the access token from the Kitsu API
119
	 *
120
	 * @param string $username
121
	 * @param string $password
122
	 * @return bool|string
123
	 */
124
	public function authenticate(string $username, string $password)
125
	{
126
		$response = $this->getResponse('POST', K::AUTH_URL, [
127
			'headers' => [],
128
			'form_params' => [
129
				'grant_type' => 'password',
130
				'username' => $username,
131
				'password' => $password
132
			]
133
		]);
134
135
		$data = Json::decode((string)$response->getBody());
136
137
		if (array_key_exists('access_token', $data))
138
		{
139
			return $data;
140
		}
141
142
		return false;
143
	}
144
145
	/**
146
	 * Get information about a particular anime
147
	 *
148
	 * @param string $slug
149
	 * @return array
150
	 */
151
	public function getAnime(string $slug): array
152
	{
153
		// @TODO catch non-existent anime
154
		$baseData = $this->getRawMediaData('anime', $slug);
155
		return $this->animeTransformer->transform($baseData);
156
	}
157
158
	/**
159
	 * Get information about a particular anime
160
	 *
161
	 * @param string $animeId
162
	 * @return array
163
	 */
164
	public function getAnimeById(string $animeId): array
165
	{
166
		$baseData = $this->getRawMediaDataById('anime', $animeId);
167
		return $this->animeTransformer->transform($baseData);
168
	}
169
170
	/**
171
	 * Get the mal id for the anime represented by the kitsu id
172
	 * to enable updating MyAnimeList
173
	 *
174
	 * @param string $kitsuAnimeId The id of the anime on Kitsu
175
	 * @return string|null Returns the mal id if it exists, otherwise null
176
	 */
177
	public function getMalIdForAnime(string $kitsuAnimeId)
178
	{
179
		$options = [
180
			'query' => [
181
				'include' => 'mappings'
182
			]
183
		];
184
		$data = $this->getRequest("anime/{$kitsuAnimeId}", $options);
185
		$mappings = array_column($data['included'], 'attributes');
186
187
		foreach($mappings as $map)
188
		{
189
			if ($map['externalSite'] === 'myanimelist/anime')
190
			{
191
				return $map['externalId'];
192
			}
193
		}
194
195
		return null;
196
	}
197
198
	/**
199
	 * Get information about a particular manga
200
	 *
201
	 * @param string $mangaId
202
	 * @return array
203
	 */
204
	public function getManga(string $mangaId): array
205
	{
206
		$baseData = $this->getRawMediaData('manga', $mangaId);
207
		return $this->mangaTransformer->transform($baseData);
208
	}
209
210
	/**
211
	 * Get and transform the entirety of the user's anime list
212
	 *
213
	 * @return array
214
	 */
215
	public function getFullAnimeList(): array
216
	{
217
218
	}
219
220
	/**
221
	 * Get the raw (unorganized) anime list for the configured user
222
	 *
223
	 * @param string $status - The watching status to filter the list with
224
	 * @param int $limit - The number of list entries to fetch for a page
225
	 * @param int $offset - The page offset
226
	 * @return array
227
	 */
228
	public function getRawAnimeList(string $status, int $limit = 600, int $offset = 0): array
229
	{
230
		$options = [
231
			'query' => [
232
				'filter' => [
233
					'user_id' => $this->getUserIdByUsername($this->getUsername()),
234
					'media_type' => 'Anime',
235
					'status' => $status,
236
				],
237
				'include' => 'media,media.genres,media.mappings,anime.streamingLinks',
238
				'page' => [
239
					'offset' => $offset,
240
					'limit' => $limit
241
				],
242
				'sort' => '-updated_at'
243
			]
244
		];
245
246
		return $this->getRequest('library-entries', $options);
247
	}
248
249
	/**
250
	 * Get the anime list for the configured user
251
	 *
252
	 * @param string $status - The watching status to filter the list with
253
	 * @param int $limit - The number of list entries to fetch for a page
254
	 * @param int $offset - The page offset
255
	 * @return array
256
	 */
257
	public function getAnimeList(string $status, int $limit = 600, int $offset = 0): array
258
	{
259
		$cacheItem = $this->cache->getItem($this->getHashForMethodCall($this, __METHOD__, [$status]));
260
261
		if ( ! $cacheItem->isHit())
262
		{
263
			$data = $this->getRawAnimeList($status, $limit, $offset);
264
			$included = JsonAPI::organizeIncludes($data['included']);
265
			$included = JsonAPI::inlineIncludedRelationships($included, 'anime');
266
267
			foreach($data['data'] as $i => &$item)
268
			{
269
				$item['included'] = $included;
270
			}
271
			$transformed = $this->animeListTransformer->transformCollection($data['data']);
272
273
			$cacheItem->set($transformed);
274
			$cacheItem->save();
275
		}
276
277
		return $cacheItem->get();
278
	}
279
280
	/**
281
	 * Get the manga list for the configured user
282
	 *
283
	 * @param string $status - The reading status by which to filter the list
284
	 * @param int $limit - The number of list items to fetch per page
285
	 * @param int $offset - The page offset
286
	 * @return array
287
	 */
288
	public function getMangaList(string $status, int $limit = 200, int $offset = 0): array
289
	{
290
		$options = [
291
			'query' => [
292
				'filter' => [
293
					'user_id' => $this->getUserIdByUsername($this->getUsername()),
294
					'media_type' => 'Manga',
295
					'status' => $status,
296
				],
297
				'include' => 'media',
298
				'page' => [
299
					'offset' => $offset,
300
					'limit' => $limit
301
				],
302
				'sort' => '-updated_at'
303
			]
304
		];
305
306
		$cacheItem = $this->cache->getItem($this->getHashForMethodCall($this, __METHOD__, $options));
307
308
		if ( ! $cacheItem->isHit())
309
		{
310
			$data = $this->getRequest('library-entries', $options);
311
			$data = JsonAPI::inlineRawIncludes($data, 'manga');
312
313
			$transformed = $this->mangaListTransformer->transformCollection($data);
314
315
			$cacheItem->set($transformed);
316
			$cacheItem->save();
317
		}
318
319
		return $cacheItem->get();
320
	}
321
322
	/**
323
	 * Search for an anime or manga
324
	 *
325
	 * @param string $type - 'anime' or 'manga'
326
	 * @param string $query - name of the item to search for
327
	 * @return array
328
	 */
329
	public function search(string $type, string $query): array
330
	{
331
		$options = [
332
			'query' => [
333
				'filter' => [
334
					'text' => $query
335
				],
336
				'page' => [
337
					'offset' => 0,
338
					'limit' => 20
339
				],
340
			]
341
		];
342
343
		$raw = $this->getRequest($type, $options);
344
345
		foreach ($raw['data'] as &$item)
346
		{
347
			$item['attributes']['titles'] = K::filterTitles($item['attributes']);
348
			array_shift($item['attributes']['titles']);
349
		}
350
351
		return $raw;
352
	}
353
354
	/**
355
	 * Create a list item
356
	 *
357
	 * @param array $data
358
	 * @return bool
359
	 */
360
	public function createListItem(array $data): bool
361
	{
362
		$data['user_id'] = $this->getUserIdByUsername($this->getUsername());
363
		return $this->listItem->create($data);
364
	}
365
366
	/**
367
	 * Get the data for a specific list item, generally for editing
368
	 *
369
	 * @param string $listId - The unique identifier of that list item
370
	 * @return array
371
	 */
372
	public function getListItem(string $listId): array
373
	{
374
		$baseData = $this->listItem->get($listId);
375
		$included = JsonAPI::organizeIncludes($baseData['included']);
376
377
378
		switch (TRUE)
379
		{
380
			case in_array('anime', array_keys($included)):
381
				$included = JsonAPI::inlineIncludedRelationships($included, 'anime');
382
				$baseData['data']['included'] = $included;
383
				return $this->animeListTransformer->transform($baseData['data']);
384
385
			case in_array('manga', array_keys($included)):
386
				$included = JsonAPI::inlineIncludedRelationships($included, 'manga');
387
				$baseData['data']['included'] = $included;
388
				$baseData['data']['manga'] = $baseData['included'][0];
389
				return $this->mangaListTransformer->transform($baseData['data']);
390
391
			default:
392
				return $baseData['data'];
393
		}
394
	}
395
396
	/**
397
	 * Modify a list item
398
	 *
399
	 * @param array $data
400
	 * @return array
401
	 */
402
	public function updateListItem(array $data)
403
	{
404
		try
405
		{
406
			$response = $this->listItem->update($data['id'], $data['data']);
407
			return [
408
				'statusCode' => $response->getStatusCode(),
409
				'body' => $response->getBody(),
410
			];
411
		}
412
		catch(ClientException $e)
413
		{
414
			return [
415
				'statusCode' => $e->getResponse()->getStatusCode(),
416
				'body' => Json::decode((string)$e->getResponse()->getBody())
417
			];
418
		}
419
	}
420
421
	/**
422
	 * Remove a list item
423
	 *
424
	 * @param string $id - The id of the list item to remove
425
	 * @return bool
426
	 */
427
	public function deleteListItem(string $id): bool
428
	{
429
		return $this->listItem->delete($id);
430
	}
431
432
	private function getUsername(): string
433
	{
434
		return $this->getContainer()
435
			->get('config')
436
			->get(['kitsu_username']);
437
	}
438
439
	private function getRawMediaDataById(string $type, string $id): array
440
	{
441
		$options = [
442
			'query' => [
443
				'include' => ($type === 'anime')
444
					? 'genres,mappings,streamingLinks'
445
					: 'genres,mappings',
446
			]
447
		];
448
449
		$data = $this->getRequest("{$type}/{$id}", $options);
450
		$baseData = $data['data']['attributes'];
451
		$baseData['included'] = $data['included'];
452
		return $baseData;
453
	}
454
455
	private function getRawMediaData(string $type, string $slug): array
456
	{
457
		$options = [
458
			'query' => [
459
				'filter' => [
460
					'slug' => $slug
461
				],
462
				'include' => ($type === 'anime')
463
					? 'genres,mappings,streamingLinks'
464
					: 'genres,mappings',
465
			]
466
		];
467
468
		$data = $this->getRequest($type, $options);
469
		$baseData = $data['data'][0]['attributes'];
470
		$baseData['included'] = $data['included'];
471
		return $baseData;
472
	}
473
}