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; |
18
|
|
|
|
19
|
|
|
use Aviat\AnimeClient\API\Kitsu\Enum\{ |
20
|
|
|
AnimeAiringStatus, |
21
|
|
|
AnimeWatchingStatus, |
22
|
|
|
MangaReadingStatus |
23
|
|
|
}; |
24
|
|
|
use DateTimeImmutable; |
25
|
|
|
|
26
|
|
|
const AUTH_URL = 'https://kitsu.io/api/oauth/token'; |
27
|
|
|
const AUTH_USER_ID_KEY = 'kitsu-auth-userid'; |
28
|
|
|
const AUTH_TOKEN_CACHE_KEY = 'kitsu-auth-token'; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* Data massaging helpers for the Kitsu API |
32
|
|
|
*/ |
33
|
|
|
class Kitsu { |
34
|
|
|
const AUTH_URL = 'https://kitsu.io/api/oauth/token'; |
35
|
|
|
const AUTH_USER_ID_KEY = 'kitsu-auth-userid'; |
36
|
|
|
const AUTH_TOKEN_CACHE_KEY = 'kitsu-auth-token'; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* Map of Kitsu status to label for select menus |
40
|
|
|
* |
41
|
|
|
* @return array |
42
|
|
|
*/ |
43
|
|
View Code Duplication |
public static function getStatusToSelectMap() |
|
|
|
|
44
|
|
|
{ |
45
|
|
|
return [ |
46
|
|
|
AnimeWatchingStatus::WATCHING => 'Currently Watching', |
47
|
|
|
AnimeWatchingStatus::PLAN_TO_WATCH => 'Plan to Watch', |
48
|
|
|
AnimeWatchingStatus::COMPLETED => 'Completed', |
49
|
|
|
AnimeWatchingStatus::ON_HOLD => 'On Hold', |
50
|
|
|
AnimeWatchingStatus::DROPPED => 'Dropped' |
51
|
|
|
]; |
52
|
|
|
} |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* Map of Kitsu Manga status to label for select menus |
56
|
|
|
* |
57
|
|
|
* @return array |
58
|
|
|
*/ |
59
|
|
View Code Duplication |
public static function getStatusToMangaSelectMap() |
|
|
|
|
60
|
|
|
{ |
61
|
|
|
return [ |
62
|
|
|
MangaReadingStatus::READING => 'Currently Reading', |
63
|
|
|
MangaReadingStatus::PLAN_TO_READ => 'Plan to Read', |
64
|
|
|
MangaReadingStatus::COMPLETED => 'Completed', |
65
|
|
|
MangaReadingStatus::ON_HOLD => 'On Hold', |
66
|
|
|
MangaReadingStatus::DROPPED => 'Dropped' |
67
|
|
|
]; |
68
|
|
|
} |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* Determine whether an anime is airing, finished airing, or has not yet aired |
72
|
|
|
* |
73
|
|
|
* @param string $startDate |
|
|
|
|
74
|
|
|
* @param string $endDate |
|
|
|
|
75
|
|
|
* @return string |
76
|
|
|
*/ |
77
|
|
|
public static function getAiringStatus(string $startDate = null, string $endDate = null): string |
78
|
|
|
{ |
79
|
|
|
$startAirDate = new DateTimeImmutable($startDate ?? 'tomorrow'); |
80
|
|
|
$endAirDate = new DateTimeImmutable($endDate ?? 'next year'); |
81
|
|
|
$now = new DateTimeImmutable(); |
82
|
|
|
|
83
|
|
|
$isDoneAiring = $now > $endAirDate; |
84
|
|
|
$isCurrentlyAiring = ($now > $startAirDate) && ! $isDoneAiring; |
85
|
|
|
|
86
|
|
|
switch (true) |
87
|
|
|
{ |
88
|
|
|
case $isCurrentlyAiring: |
|
|
|
|
89
|
|
|
return AnimeAiringStatus::AIRING; |
90
|
|
|
|
91
|
|
|
case $isDoneAiring: |
|
|
|
|
92
|
|
|
return AnimeAiringStatus::FINISHED_AIRING; |
93
|
|
|
|
94
|
|
|
default: |
|
|
|
|
95
|
|
|
return AnimeAiringStatus::NOT_YET_AIRED; |
96
|
|
|
} |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
/** |
100
|
|
|
* Get the name and logo for the streaming service of the current link |
101
|
|
|
* |
102
|
|
|
* @param string $hostname |
|
|
|
|
103
|
|
|
* @return array |
104
|
|
|
*/ |
105
|
|
|
protected static function getServiceMetaData(string $hostname = null): array |
106
|
|
|
{ |
107
|
|
|
switch($hostname) |
108
|
|
|
{ |
109
|
|
|
case 'www.crunchyroll.com': |
110
|
|
|
return [ |
111
|
|
|
'name' => 'Crunchyroll', |
112
|
|
|
'link' => true, |
113
|
|
|
'logo' => '<svg class="streaming-logo" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><g fill="#F78B24" fill-rule="evenodd"><path d="M22.549 49.145c-.815-.077-2.958-.456-3.753-.663-6.873-1.79-12.693-6.59-15.773-13.009C1.335 31.954.631 28.807.633 24.788c.003-4.025.718-7.235 2.38-10.686 1.243-2.584 2.674-4.609 4.706-6.66 3.8-3.834 8.614-6.208 14.067-6.936 1.783-.239 5.556-.161 7.221.148 3.463.642 6.571 1.904 9.357 3.797 5.788 3.934 9.542 9.951 10.52 16.861.21 1.48.332 4.559.19 4.816-.077.14-.117-.007-.167-.615-.25-3.015-1.528-6.66-3.292-9.388C40.253 7.836 30.249 4.32 20.987 7.467c-7.15 2.43-12.522 8.596-13.997 16.06-.73 3.692-.51 7.31.658 10.882a21.426 21.426 0 0 0 13.247 13.518c1.475.515 3.369.944 4.618 1.047 1.496.122 1.119.239-.727.224-1.006-.008-2.013-.032-2.237-.053z"></path><path d="M27.685 46.1c-7.731-.575-14.137-6.455-15.474-14.204-.243-1.41-.29-4.047-.095-5.345 1.16-7.706 6.97-13.552 14.552-14.639 1.537-.22 4.275-.143 5.746.162 1.28.266 2.7.737 3.814 1.266l.865.411-.814.392c-2.936 1.414-4.748 4.723-4.323 7.892.426 3.173 2.578 5.664 5.667 6.56 1.112.322 2.812.322 3.925 0 1.438-.417 2.566-1.1 3.593-2.173.346-.362.652-.621.68-.576.027.046.106.545.176 1.11.171 1.395.07 4.047-.204 5.371-.876 4.218-3.08 7.758-6.463 10.374-3.2 2.476-7.434 3.711-11.645 3.399z"></path></g></svg>' |
|
|
|
|
114
|
|
|
]; |
115
|
|
|
|
116
|
|
|
case 'www.funimation.com': |
117
|
|
|
return [ |
118
|
|
|
'name' => 'Funimation', |
119
|
|
|
'link' => true, |
120
|
|
|
'logo' => '<svg class="streaming-logo" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><path d="M24.066.017a24.922 24.922 0 0 1 13.302 3.286 25.098 25.098 0 0 1 7.833 7.058 24.862 24.862 0 0 1 4.207 9.575c.82 4.001.641 8.201-.518 12.117a24.946 24.946 0 0 1-4.868 9.009 24.98 24.98 0 0 1-7.704 6.118 24.727 24.727 0 0 1-10.552 2.718A24.82 24.82 0 0 1 13.833 47.3c-5.815-2.872-10.408-8.107-12.49-14.25-2.162-6.257-1.698-13.375 1.303-19.28C5.483 8.07 10.594 3.55 16.602 1.435A24.94 24.94 0 0 1 24.066.017zm-8.415 33.31c.464 2.284 1.939 4.358 3.99 5.48 2.174 1.217 4.765 1.444 7.202 1.181 2.002-.217 3.986-.992 5.455-2.397 1.173-1.151 2.017-2.648 2.33-4.267-1.189-.027-2.378 0-3.566-.03-.568.082-1.137-.048-1.705.014-1.232.012-2.465.003-3.697-.01-.655.066-1.309-.035-1.963.013-1.166-.053-2.334.043-3.5-.025-1.515.08-3.03-.035-4.546.042z" fill="#411299" fill-rule="evenodd"></path></svg>' |
|
|
|
|
121
|
|
|
]; |
122
|
|
|
|
123
|
|
|
case 'www.hulu.com': |
124
|
|
|
return [ |
125
|
|
|
'name' => 'Hulu', |
126
|
|
|
'link' => true, |
127
|
|
|
'logo' => '<svg class="streaming-logo" viewBox="0 0 34 50" xmlns="http://www.w3.org/2000/svg"><path d="M22.222 13.889h-11.11V0H0v50h11.111V27.778c0-1.39 1.111-2.778 2.778-2.778h5.555c1.39 0 2.778 1.111 2.778 2.778V50h11.111V25c0-6.111-5-11.111-11.11-11.111z" fill="#8BC34A" fill-rule="evenodd"></path></svg>' |
|
|
|
|
128
|
|
|
]; |
129
|
|
|
|
130
|
|
|
// Default to Netflix, because the API links are broken, |
131
|
|
|
// and there's no other real identifier for Netflix |
132
|
|
|
default: |
133
|
|
|
return [ |
134
|
|
|
'name' => 'Netflix', |
135
|
|
|
'link' => false, |
136
|
|
|
'logo' => '<svg class="streaming-logo" viewBox="0 0 26 50" xmlns="http://www.w3.org/2000/svg"><path d="M.057.258C2.518.253 4.982.263 7.446.253c2.858 7.76 5.621 15.556 8.456 23.324.523 1.441 1.003 2.897 1.59 4.312.078-9.209.01-18.42.034-27.631h7.763v46.36c-2.812.372-5.637.627-8.457.957-1.203-3.451-2.396-6.902-3.613-10.348-1.796-5.145-3.557-10.302-5.402-15.428.129 8.954.015 17.912.057 26.871-2.603.39-5.227.637-7.815 1.119C.052 33.279.06 16.768.057.258z" fill="#E21221" fill-rule="evenodd"></path></svg>' |
|
|
|
|
137
|
|
|
]; |
138
|
|
|
} |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
/** |
142
|
|
|
* Reorganize streaming links |
143
|
|
|
* |
144
|
|
|
* @param array $included |
145
|
|
|
* @return array |
146
|
|
|
*/ |
147
|
|
|
public static function parseStreamingLinks(array $included): array |
148
|
|
|
{ |
149
|
|
|
if ( ! array_key_exists('streamingLinks', $included)) |
150
|
|
|
{ |
151
|
|
|
return []; |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
$links = []; |
155
|
|
|
|
156
|
|
View Code Duplication |
foreach ($included['streamingLinks'] as $streamingLink) |
|
|
|
|
157
|
|
|
{ |
158
|
|
|
$host = parse_url($streamingLink['url'], \PHP_URL_HOST); |
159
|
|
|
|
160
|
|
|
$links[] = [ |
161
|
|
|
'meta' => static::getServiceMetaData($host), |
|
|
|
|
162
|
|
|
'link' => $streamingLink['url'], |
163
|
|
|
'subs' => $streamingLink['subs'], |
164
|
|
|
'dubs' => $streamingLink['dubs'] |
165
|
|
|
]; |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
return $links; |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
/** |
172
|
|
|
* Reorganize streaming links for the current list item |
173
|
|
|
* |
174
|
|
|
* @param array $included |
175
|
|
|
* @return array |
176
|
|
|
*/ |
177
|
|
|
public static function parseListItemStreamingLinks(array $included, string $animeId): array |
178
|
|
|
{ |
179
|
|
|
// Anime lists have a different structure to search through |
180
|
|
|
if (array_key_exists('anime', $included) && ! array_key_exists('streamingLinks', $included)) |
181
|
|
|
{ |
182
|
|
|
$links = []; |
183
|
|
|
$anime = $included['anime'][$animeId]; |
184
|
|
|
|
185
|
|
|
if (count($anime['relationships']['streamingLinks']) > 0) |
186
|
|
|
{ |
187
|
|
View Code Duplication |
foreach ($anime['relationships']['streamingLinks'] as $streamingLink) |
|
|
|
|
188
|
|
|
{ |
189
|
|
|
$host = parse_url($streamingLink['url'], \PHP_URL_HOST); |
190
|
|
|
|
191
|
|
|
$links[] = [ |
192
|
|
|
'meta' => static::getServiceMetaData($host), |
|
|
|
|
193
|
|
|
'link' => $streamingLink['url'], |
194
|
|
|
'subs' => $streamingLink['subs'], |
195
|
|
|
'dubs' => $streamingLink['dubs'] |
196
|
|
|
]; |
197
|
|
|
} |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
return $links; |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
return []; |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
/** |
207
|
|
|
* Filter out duplicate and very similar names from |
208
|
|
|
* |
209
|
|
|
* @param array $data The 'attributes' section of the api data response |
210
|
|
|
* @return array List of alternate titles |
211
|
|
|
*/ |
212
|
|
|
public static function filterTitles(array $data): array |
213
|
|
|
{ |
214
|
|
|
// The 'canonical' title is always returned |
215
|
|
|
$valid = [$data['canonicalTitle']]; |
216
|
|
|
|
217
|
|
|
if (array_key_exists('titles', $data)) |
218
|
|
|
{ |
219
|
|
|
foreach($data['titles'] as $alternateTitle) |
220
|
|
|
{ |
221
|
|
|
if (self::titleIsUnique($alternateTitle, $valid)) |
222
|
|
|
{ |
223
|
|
|
$valid[] = $alternateTitle; |
224
|
|
|
} |
225
|
|
|
} |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
return $valid; |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
/** |
232
|
|
|
* Determine if an alternate title is unique enough to list |
233
|
|
|
* |
234
|
|
|
* @param string $title |
|
|
|
|
235
|
|
|
* @param array $existingTitles |
236
|
|
|
* @return bool |
237
|
|
|
*/ |
238
|
|
|
private static function titleIsUnique(string $title = null, array $existingTitles): bool |
|
|
|
|
239
|
|
|
{ |
240
|
|
|
if (empty($title)) |
241
|
|
|
{ |
242
|
|
|
return false; |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
foreach($existingTitles as $existing) |
246
|
|
|
{ |
247
|
|
|
$isSubset = stripos($existing, $title) !== FALSE; |
248
|
|
|
$diff = levenshtein($existing, $title); |
249
|
|
|
$onlydifferentCase = (mb_strtolower($existing) === mb_strtolower($title)); |
250
|
|
|
|
251
|
|
|
if ($diff < 3 || $isSubset || $onlydifferentCase) |
252
|
|
|
{ |
253
|
|
|
return false; |
254
|
|
|
} |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
return true; |
258
|
|
|
} |
259
|
|
|
} |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.