Model   A
last analyzed

Complexity

Total Complexity 32

Size/Duplication

Total Lines 422
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Importance

Changes 0
Metric Value
dl 0
loc 422
rs 9.6
c 0
b 0
f 0
wmc 32
lcom 1
cbo 12

19 Methods

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