1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* AnimeDb package. |
4
|
|
|
* |
5
|
|
|
* @author Peter Gribanov <[email protected]> |
6
|
|
|
* @copyright Copyright (c) 2011, Peter Gribanov |
7
|
|
|
* @license http://opensource.org/licenses/GPL-3.0 GPL v3 |
8
|
|
|
*/ |
9
|
|
|
namespace AnimeDb\Bundle\AniDbFillerBundle\Service; |
10
|
|
|
|
11
|
|
|
use AnimeDb\Bundle\CatalogBundle\Plugin\Fill\Filler\Filler as FillerPlugin; |
12
|
|
|
use AnimeDb\Bundle\AniDbBrowserBundle\Service\Browser; |
13
|
|
|
use Doctrine\Bundle\DoctrineBundle\Registry; |
14
|
|
|
use AnimeDb\Bundle\AppBundle\Service\Downloader; |
15
|
|
|
use AnimeDb\Bundle\CatalogBundle\Entity\Item; |
16
|
|
|
use AnimeDb\Bundle\CatalogBundle\Entity\Name; |
17
|
|
|
use AnimeDb\Bundle\CatalogBundle\Entity\Source; |
18
|
|
|
use AnimeDb\Bundle\CatalogBundle\Entity\Genre; |
19
|
|
|
use AnimeDb\Bundle\AniDbFillerBundle\Form\Type\Filler as FillerForm; |
20
|
|
|
use Symfony\Component\DomCrawler\Crawler; |
21
|
|
|
use Knp\Menu\ItemInterface; |
22
|
|
|
use AnimeDb\Bundle\AppBundle\Service\Downloader\Entity\EntityInterface; |
23
|
|
|
|
24
|
|
|
class Filler extends FillerPlugin |
25
|
|
|
{ |
26
|
|
|
/** |
27
|
|
|
* @var string |
28
|
|
|
*/ |
29
|
|
|
const NAME = 'anidb'; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* @var string |
33
|
|
|
*/ |
34
|
|
|
const TITLE = 'AniDB.net'; |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* RegExp for get item id. |
38
|
|
|
* |
39
|
|
|
* @var string |
40
|
|
|
*/ |
41
|
|
|
const REG_ITEM_ID = '#/perl\-bin/animedb\.pl\?show=anime&aid=(?<id>\d+)#'; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* @var Browser |
45
|
|
|
*/ |
46
|
|
|
protected $browser; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* @var Registry |
50
|
|
|
*/ |
51
|
|
|
protected $doctrine; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* @var Downloader |
55
|
|
|
*/ |
56
|
|
|
protected $downloader; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* @var SummaryCleaner |
60
|
|
|
*/ |
61
|
|
|
protected $cleaner; |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* @var string |
65
|
|
|
*/ |
66
|
|
|
protected $locale; |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* AniDB category to genre. |
70
|
|
|
* |
71
|
|
|
* <code> |
72
|
|
|
* { from: to, ... } |
73
|
|
|
* </code> |
74
|
|
|
* |
75
|
|
|
* @var array |
76
|
|
|
*/ |
77
|
|
|
protected $category_to_genre = [ |
78
|
|
|
'Alternative History' => 'History', |
79
|
|
|
'Anti-War' => 'War', |
80
|
|
|
'Badminton' => 'Sport', |
81
|
|
|
'Bakumatsu - Meiji Period' => 'History', |
82
|
|
|
'Band' => 'Music', |
83
|
|
|
'Baseball' => 'Sport', |
84
|
|
|
'Basketball' => 'Sport', |
85
|
|
|
'Battle Royale' => 'War', |
86
|
|
|
'Board Games' => 'Game', |
87
|
|
|
'Boxing' => 'Sport', |
88
|
|
|
'Catholic School' => 'School', |
89
|
|
|
'Chess' => 'Sport', |
90
|
|
|
'Clubs' => 'School', |
91
|
|
|
'College' => 'School', |
92
|
|
|
'Combat' => 'Action', |
93
|
|
|
'Conspiracy' => 'Thriller', |
94
|
|
|
'Contemporary Fantasy' => 'Fantasy', |
95
|
|
|
'Cops' => 'Police', |
96
|
|
|
'Daily Life' => 'Slice of life', |
97
|
|
|
'Dark Elf' => 'Fantasy', |
98
|
|
|
'Dark Fantasy' => 'Fantasy', |
99
|
|
|
'Dodgeball' => 'Sport', |
100
|
|
|
'Dragon' => 'Fantasy', |
101
|
|
|
'Edo Period' => 'Fantasy', |
102
|
|
|
'Elementary School' => 'School', |
103
|
|
|
'Elf' => 'Fantasy', |
104
|
|
|
'Fairies' => 'Fantasy', |
105
|
|
|
'Fantasy World' => 'Fantasy', |
106
|
|
|
'Feudal Warfare' => 'War', |
107
|
|
|
'Football' => 'Sport', |
108
|
|
|
'Formula Racing' => 'Sport', |
109
|
|
|
'Ghost' => 'Supernatural', |
110
|
|
|
'Go' => 'Game', |
111
|
|
|
'Golf' => 'Sport', |
112
|
|
|
'Gunfights' => 'War', |
113
|
|
|
'Gymnastics' => 'Sport', |
114
|
|
|
'Heian Period' => 'History', |
115
|
|
|
'High Fantasy' => 'Fantasy', |
116
|
|
|
'High School' => 'School', |
117
|
|
|
'Historical' => 'History', |
118
|
|
|
'Ice Skating' => 'Sport', |
119
|
|
|
'Inline Skating' => 'Sport', |
120
|
|
|
'Jousting' => 'Sport', |
121
|
|
|
'Judo' => 'Sport', |
122
|
|
|
'Kendo' => 'Sport', |
123
|
|
|
'Law and Order' => 'Police', |
124
|
|
|
'Magic Circles' => 'Magic', |
125
|
|
|
'Mahjong' => 'Game', |
126
|
|
|
'Mahou Shoujo' => 'Mahoe shoujo', |
127
|
|
|
'Martial Arts' => 'Martial arts', |
128
|
|
|
'Military' => 'War', |
129
|
|
|
'Motorsport' => 'Sport', |
130
|
|
|
'Muay Thai' => 'Sport', |
131
|
|
|
'Ninja' => 'Samurai', |
132
|
|
|
'Pirate' => 'Adventure', |
133
|
|
|
'Post-apocalypse' => 'Apocalyptic fiction', |
134
|
|
|
'Post-War' => 'War', |
135
|
|
|
'Proxy Battles' => 'War', |
136
|
|
|
'Reverse Harem' => 'Harem', |
137
|
|
|
'Rugby' => 'Sport', |
138
|
|
|
'School Dormitory' => 'School', |
139
|
|
|
'School Excursion' => 'School', |
140
|
|
|
'School Festival' => 'School', |
141
|
|
|
'School Life' => 'School', |
142
|
|
|
'School Sports Festival' => 'School', |
143
|
|
|
'Sci-Fi' => 'Sci-fi', |
144
|
|
|
'Sengoku Period' => 'History', |
145
|
|
|
'Shougi' => 'Game', |
146
|
|
|
'Shoujo Ai' => 'Shoujo-ai', |
147
|
|
|
'Shounen Ai' => 'Shounen-ai', |
148
|
|
|
'Spellcasting' => 'Magic', |
149
|
|
|
'Sports' => 'Sport', |
150
|
|
|
'Street Racing' => 'Cars', |
151
|
|
|
'Swimming' => 'Sport', |
152
|
|
|
'Swordplay' => 'Sport', |
153
|
|
|
'Tennis' => 'Sport', |
154
|
|
|
'Victorian Period' => 'History', |
155
|
|
|
'Volleyball' => 'Sport', |
156
|
|
|
'Witch' => 'Magic', |
157
|
|
|
'World War I' => 'War', |
158
|
|
|
'World War II' => 'War', |
159
|
|
|
'Wrestling' => 'Action', |
160
|
|
|
]; |
161
|
|
|
|
162
|
|
|
/** |
163
|
|
|
* @param Browser $browser |
164
|
|
|
* @param Registry $doctrine |
165
|
|
|
* @param Downloader $downloader |
166
|
|
|
* @param SummaryCleaner $cleaner |
167
|
|
|
* @param $locale |
168
|
|
|
*/ |
169
|
|
|
public function __construct( |
170
|
|
|
Browser $browser, |
171
|
|
|
Registry $doctrine, |
172
|
|
|
Downloader $downloader, |
173
|
|
|
SummaryCleaner $cleaner, |
174
|
|
|
$locale |
175
|
|
|
) { |
176
|
|
|
$this->browser = $browser; |
177
|
|
|
$this->doctrine = $doctrine; |
178
|
|
|
$this->downloader = $downloader; |
179
|
|
|
$this->cleaner = $cleaner; |
180
|
|
|
$this->locale = $locale; |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
/** |
184
|
|
|
* @return string |
185
|
|
|
*/ |
186
|
|
|
public function getName() |
187
|
|
|
{ |
188
|
|
|
return self::NAME; |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
/** |
192
|
|
|
* @return string |
193
|
|
|
*/ |
194
|
|
|
public function getTitle() |
195
|
|
|
{ |
196
|
|
|
return self::TITLE; |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
/** |
200
|
|
|
* @return FillerForm |
201
|
|
|
*/ |
202
|
|
|
public function getForm() |
203
|
|
|
{ |
204
|
|
|
return new FillerForm($this->browser->getHost()); |
|
|
|
|
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
/** |
208
|
|
|
* Build menu for plugin. |
209
|
|
|
* |
210
|
|
|
* @param ItemInterface $item |
211
|
|
|
* |
212
|
|
|
* @return ItemInterface |
213
|
|
|
*/ |
214
|
|
|
public function buildMenu(ItemInterface $item) |
215
|
|
|
{ |
216
|
|
|
return parent::buildMenu($item) |
217
|
|
|
->setLinkAttribute('class', 'icon-label icon-label-plugin-anidb'); |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* Fill item from source. |
222
|
|
|
* |
223
|
|
|
* @param array $data |
224
|
|
|
* |
225
|
|
|
* @return Item|null |
226
|
|
|
*/ |
227
|
|
|
public function fill(array $data) |
228
|
|
|
{ |
229
|
|
|
if (empty($data['url']) || !is_string($data['url']) || |
230
|
|
|
strpos($data['url'], $this->browser->getHost()) !== 0 || |
231
|
|
|
!preg_match(self::REG_ITEM_ID, $data['url'], $match) |
232
|
|
|
) { |
233
|
|
|
return null; |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
$body = $this->browser->get('anime', ['aid' => $match['id']]); |
237
|
|
|
|
238
|
|
|
$item = new Item(); |
239
|
|
|
$item->setEpisodesNumber($body->filter('episodecount')->text()); |
240
|
|
|
$item->setDatePremiere(new \DateTime($body->filter('startdate')->text())); |
241
|
|
|
$item->setDateEnd(new \DateTime($body->filter('enddate')->text())); |
242
|
|
|
$item->setSummary($this->cleaner->clean($body->filter('description')->text())); |
243
|
|
|
|
244
|
|
|
// set main source |
245
|
|
|
$source = new Source(); |
246
|
|
|
$source->setUrl($data['url']); |
247
|
|
|
$item->addSource($source); |
248
|
|
|
|
249
|
|
|
// add url to offsite |
250
|
|
|
$source = new Source(); |
251
|
|
|
$source->setUrl($body->filter('url')->text()); |
252
|
|
|
$item->addSource($source); |
253
|
|
|
|
254
|
|
|
// set complex data |
255
|
|
|
$this->setCover($item, $body, $match['id']); |
256
|
|
|
$this->setNames($item, $body); |
257
|
|
|
$this->setEpisodes($item, $body); |
258
|
|
|
$this->setType($item, $body); |
259
|
|
|
$this->setGenres($item, $body); |
260
|
|
|
|
261
|
|
|
return $item; |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
/** |
265
|
|
|
* @param Item $item |
266
|
|
|
* @param Crawler $body |
267
|
|
|
* |
268
|
|
|
* @return Item |
269
|
|
|
*/ |
270
|
|
|
public function setNames(Item $item, Crawler $body) |
271
|
|
|
{ |
272
|
|
|
$titles = $body->filter('titles > title'); |
273
|
|
|
$names = []; |
274
|
|
|
/* @var $title \DOMElement */ |
275
|
|
|
foreach ($titles as $title) { |
276
|
|
|
$lang = substr($title->attributes->item(0)->nodeValue, 0, 2); |
277
|
|
|
if ($lang != 'x-') { |
278
|
|
|
$names[$lang][$title->getAttribute('type')] = $title->nodeValue; |
279
|
|
|
} |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
// set main name |
283
|
|
|
if (!empty($names[$this->locale])) { |
284
|
|
|
$item->setName($this->getNameForLocale($this->locale, $names)); |
285
|
|
View Code Duplication |
} elseif ($this->locale != 'en' && !empty($names['en'])) { |
|
|
|
|
286
|
|
|
$item->setName($this->getNameForLocale('en', $names)); |
287
|
|
|
} else { |
288
|
|
|
$item->setName($this->getNameForLocale(array_keys($names)[0], $names)); |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
// set other names |
292
|
|
|
$other = []; |
293
|
|
|
foreach ($names as $locales) { |
294
|
|
|
foreach ($locales as $name) { |
295
|
|
|
$other[] = $name; |
296
|
|
|
} |
297
|
|
|
} |
298
|
|
|
$other = array_unique($other); |
299
|
|
|
sort($other); |
300
|
|
|
|
301
|
|
|
foreach ($other as $name) { |
302
|
|
|
$item->addName((new Name())->setName($name)); |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
return $item; |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
/** |
309
|
|
|
* @param Item $item |
310
|
|
|
* @param Crawler $body |
311
|
|
|
* @param string $id |
312
|
|
|
* |
313
|
|
|
* @return Item |
314
|
|
|
*/ |
315
|
|
|
public function setCover(Item $item, Crawler $body, $id) |
316
|
|
|
{ |
317
|
|
|
if ($image = $body->filter('picture')->text()) { |
318
|
|
|
try { |
319
|
|
|
$image = $this->browser->getImageUrl($image); |
320
|
|
|
if ($path = parse_url($image, PHP_URL_PATH)) { |
321
|
|
|
$ext = pathinfo($path, PATHINFO_EXTENSION); |
322
|
|
|
$item->setCover(self::NAME.'/'.$id.'/cover.'.$ext); |
323
|
|
|
$this->uploadImageFromUrl($image, $item); |
324
|
|
|
} |
325
|
|
|
} catch (\Exception $e) { |
326
|
|
|
// error while retrieving images is not critical |
327
|
|
|
} |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
return $item; |
331
|
|
|
} |
332
|
|
|
|
333
|
|
|
/** |
334
|
|
|
* @param Item $item |
335
|
|
|
* @param Crawler $body |
336
|
|
|
* |
337
|
|
|
* @return Item |
338
|
|
|
*/ |
339
|
|
|
public function setEpisodes(Item $item, Crawler $body) |
340
|
|
|
{ |
341
|
|
|
$episodes = ''; |
342
|
|
|
foreach ($body->filter('episodes > episode') as $episode) { |
343
|
|
|
$episode = new Crawler($episode); |
344
|
|
|
$episodes .= $episode->filter('epno')->text().'. '.$this->getEpisodeTitle($episode)."\n"; |
345
|
|
|
} |
346
|
|
|
$item->setEpisodes(trim($episodes)); |
347
|
|
|
|
348
|
|
|
return $item; |
349
|
|
|
} |
350
|
|
|
|
351
|
|
|
/** |
352
|
|
|
* @param Crawler $episode |
353
|
|
|
* |
354
|
|
|
* @return string |
355
|
|
|
*/ |
356
|
|
|
protected function getEpisodeTitle(Crawler $episode) |
357
|
|
|
{ |
358
|
|
|
$titles = []; |
359
|
|
|
/* @var $title \DOMElement */ |
360
|
|
|
foreach ($episode->filter('title') as $title) { |
361
|
|
|
$lang = substr($title->attributes->item(0)->nodeValue, 0, 2); |
362
|
|
|
if ($lang == $this->locale) { |
363
|
|
|
return $title->nodeValue; |
364
|
|
|
} |
365
|
|
|
if ($lang != 'x-') { |
366
|
|
|
$titles[$lang] = $title->nodeValue; |
367
|
|
|
} |
368
|
|
|
} |
369
|
|
|
|
370
|
|
|
// get EN lang or first |
371
|
|
|
if (!empty($titles['en'])) { |
372
|
|
|
return $titles['en']; |
373
|
|
|
} else { |
374
|
|
|
return array_shift($titles); |
375
|
|
|
} |
376
|
|
|
} |
377
|
|
|
|
378
|
|
|
/** |
379
|
|
|
* @param Item $item |
380
|
|
|
* @param Crawler $body |
381
|
|
|
* |
382
|
|
|
* @return Item |
383
|
|
|
*/ |
384
|
|
|
public function setType(Item $item, Crawler $body) |
385
|
|
|
{ |
386
|
|
|
$rename = [ |
387
|
|
|
'TV Series' => 'TV', |
388
|
|
|
'Movie' => 'Feature', |
389
|
|
|
'Web' => 'ONA', |
390
|
|
|
]; |
391
|
|
|
$type = $body->filter('anime > type')->text(); |
392
|
|
|
$type = isset($rename[$type]) ? $rename[$type] : $type; |
393
|
|
|
|
394
|
|
|
return $item->setType( |
395
|
|
|
$this |
396
|
|
|
->doctrine |
397
|
|
|
->getRepository('AnimeDbCatalogBundle:Type') |
398
|
|
|
->findOneBy(['name' => $type]) |
399
|
|
|
); |
400
|
|
|
} |
401
|
|
|
|
402
|
|
|
/** |
403
|
|
|
* @param Item $item |
404
|
|
|
* @param Crawler $body |
405
|
|
|
* |
406
|
|
|
* @return Item |
407
|
|
|
*/ |
408
|
|
|
public function setGenres(Item $item, Crawler $body) |
409
|
|
|
{ |
410
|
|
|
$repository = $this->doctrine->getRepository('AnimeDbCatalogBundle:Genre'); |
411
|
|
|
$categories = $body->filter('categories > category > name'); |
412
|
|
|
foreach ($categories as $category) { |
413
|
|
|
if (isset($this->category_to_genre[$category->nodeValue])) { |
414
|
|
|
$genre = $repository->findOneBy(['name' => $this->category_to_genre[$category->nodeValue]]); |
415
|
|
|
} else { |
416
|
|
|
$genre = $repository->findOneBy(['name' => $category->nodeValue]); |
417
|
|
|
} |
418
|
|
|
if ($genre instanceof Genre) { |
419
|
|
|
$item->addGenre($genre); |
420
|
|
|
} |
421
|
|
|
} |
422
|
|
|
|
423
|
|
|
return $item; |
424
|
|
|
} |
425
|
|
|
|
426
|
|
|
/** |
427
|
|
|
* @param string $url |
428
|
|
|
* @param EntityInterface $entity |
429
|
|
|
* |
430
|
|
|
* @return bool |
431
|
|
|
*/ |
432
|
|
|
protected function uploadImageFromUrl($url, EntityInterface $entity) |
433
|
|
|
{ |
434
|
|
|
return $this->downloader->image($url, $this->downloader->getRoot().$entity->getWebPath()); |
435
|
|
|
} |
436
|
|
|
|
437
|
|
|
/** |
438
|
|
|
* @param string $locale |
439
|
|
|
* @param array $names |
440
|
|
|
* |
441
|
|
|
* @return string |
442
|
|
|
*/ |
443
|
|
View Code Duplication |
protected function getNameForLocale($locale, &$names) |
|
|
|
|
444
|
|
|
{ |
445
|
|
|
if (isset($names[$locale]['main'])) { |
446
|
|
|
$name = $names[$locale]['main']; |
447
|
|
|
unset($names[$locale]['main']); |
448
|
|
|
} elseif (isset($names[$locale]['official'])) { |
449
|
|
|
$name = $names[$locale]['official']; |
450
|
|
|
unset($names[$locale]['official']); |
451
|
|
|
} else { |
452
|
|
|
$name = array_shift($names[$locale]); |
453
|
|
|
} |
454
|
|
|
|
455
|
|
|
return $name; |
456
|
|
|
} |
457
|
|
|
|
458
|
|
|
/** |
459
|
|
|
* @param string $url |
460
|
|
|
* |
461
|
|
|
* @return bool |
462
|
|
|
*/ |
463
|
|
|
public function isSupportedUrl($url) |
464
|
|
|
{ |
465
|
|
|
return strpos($url, $this->browser->getHost()) === 0; |
466
|
|
|
} |
467
|
|
|
} |
468
|
|
|
|
If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.
Let’s take a look at an example:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.