Completed
Push — v0.1.10 ( f1c758...c6bdea )
by Peter
22:00
created

Filler::uploadImageFromUrl()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 3
ccs 0
cts 2
cp 0
rs 10
cc 1
eloc 2
nc 1
nop 2
crap 2
1
<?php
2
/**
3
 * AnimeDb package
4
 *
5
 * @package   AnimeDb
6
 * @author    Peter Gribanov <[email protected]>
7
 * @copyright Copyright (c) 2011, Peter Gribanov
8
 * @license   http://opensource.org/licenses/GPL-3.0 GPL v3
9
 */
10
11
namespace AnimeDb\Bundle\AniDbFillerBundle\Service;
12
13
use AnimeDb\Bundle\CatalogBundle\Plugin\Fill\Filler\Filler as FillerPlugin;
14
use AnimeDb\Bundle\AniDbBrowserBundle\Service\Browser;
15
use Doctrine\Bundle\DoctrineBundle\Registry;
16
use AnimeDb\Bundle\AppBundle\Service\Downloader;
17
use AnimeDb\Bundle\CatalogBundle\Entity\Item;
18
use AnimeDb\Bundle\CatalogBundle\Entity\Name;
19
use AnimeDb\Bundle\CatalogBundle\Entity\Source;
20
use AnimeDb\Bundle\CatalogBundle\Entity\Genre;
21
use AnimeDb\Bundle\AniDbFillerBundle\Form\Type\Filler as FillerForm;
22
use Symfony\Component\DomCrawler\Crawler;
23
use Knp\Menu\ItemInterface;
24
use AnimeDb\Bundle\AppBundle\Service\Downloader\Entity\EntityInterface;
25
26
class Filler extends FillerPlugin
27
{
28
    /**
29
     * @var string
30
     */
31
    const NAME = 'anidb';
32
33
    /**
34
     * @var string
35
     */
36
    const TITLE = 'AniDB.net';
37
38
    /**
39
     * RegExp for get item id
40
     *
41
     * @var string
42
     */
43
    const REG_ITEM_ID = '#/perl\-bin/animedb\.pl\?show=anime&aid=(?<id>\d+)#';
44
45
    /**
46
     * @var Browser
47
     */
48
    private $browser;
49
50
    /**
51
     * @var Registry
52
     */
53
    private $doctrine;
54
55
    /**
56
     * @var Downloader
57
     */
58
    private $downloader;
59
60
    /**
61
     * @var string
62
     */
63
    protected $locale;
64
65
    /**
66
     * AniDB category to genre
67
     *
68
     * <code>
69
     *     { from: to, ... }
70
     * </code>
71
     *
72
     * @var array
73
     */
74
    protected $category_to_genre = [
75
        'Alternative History' => 'History',
76
        'Anti-War' => 'War',
77
        'Badminton' => 'Sport',
78
        'Bakumatsu - Meiji Period' => 'History',
79
        'Band' => 'Music',
80
        'Baseball' => 'Sport',
81
        'Basketball' => 'Sport',
82
        'Battle Royale' => 'War',
83
        'Board Games' => 'Game',
84
        'Boxing' => 'Sport',
85
        'Catholic School' => 'School',
86
        'Chess' => 'Sport',
87
        'Clubs' => 'School',
88
        'College' => 'School',
89
        'Combat' => 'Action',
90
        'Conspiracy' => 'Thriller',
91
        'Contemporary Fantasy' => 'Fantasy',
92
        'Cops' => 'Police',
93
        'Daily Life' => 'Slice of life',
94
        'Dark Elf' => 'Fantasy',
95
        'Dark Fantasy' => 'Fantasy',
96
        'Dodgeball' => 'Sport',
97
        'Dragon' => 'Fantasy',
98
        'Edo Period' => 'Fantasy',
99
        'Elementary School' => 'School',
100
        'Elf' => 'Fantasy',
101
        'Fairies' => 'Fantasy',
102
        'Fantasy World' => 'Fantasy',
103
        'Feudal Warfare' => 'War',
104
        'Football' => 'Sport',
105
        'Formula Racing' => 'Sport',
106
        'Ghost' => 'Supernatural',
107
        'Go' => 'Game',
108
        'Golf' => 'Sport',
109
        'Gunfights' => 'War',
110
        'Gymnastics' => 'Sport',
111
        'Heian Period' => 'History',
112
        'High Fantasy' => 'Fantasy',
113
        'High School' => 'School',
114
        'Historical' => 'History',
115
        'Ice Skating' => 'Sport',
116
        'Inline Skating' => 'Sport',
117
        'Jousting' => 'Sport',
118
        'Judo' => 'Sport',
119
        'Kendo' => 'Sport',
120
        'Law and Order' => 'Police',
121
        'Magic Circles' => 'Magic',
122
        'Mahjong' => 'Game',
123
        'Mahou Shoujo' => 'Mahoe shoujo',
124
        'Martial Arts' => 'Martial arts',
125
        'Military' => 'War',
126
        'Motorsport' => 'Sport',
127
        'Muay Thai' => 'Sport',
128
        'Ninja' => 'Samurai',
129
        'Pirate' => 'Adventure',
130
        'Post-apocalypse' => 'Apocalyptic fiction',
131
        'Post-War' => 'War',
132
        'Proxy Battles' => 'War',
133
        'Reverse Harem' => 'Harem',
134
        'Rugby' => 'Sport',
135
        'School Dormitory' => 'School',
136
        'School Excursion' => 'School',
137
        'School Festival' => 'School',
138
        'School Life' => 'School',
139
        'School Sports Festival' => 'School',
140
        'Sci-Fi' => 'Sci-fi',
141
        'Sengoku Period' => 'History',
142
        'Shougi' => 'Game',
143
        'Shoujo Ai' => 'Shoujo-ai',
144
        'Shounen Ai' => 'Shounen-ai',
145
        'Spellcasting' => 'Magic',
146
        'Sports' => 'Sport',
147
        'Street Racing' => 'Cars',
148
        'Swimming' => 'Sport',
149
        'Swordplay' => 'Sport',
150
        'Tennis' => 'Sport',
151
        'Victorian Period' => 'History',
152
        'Volleyball' => 'Sport',
153
        'Witch' => 'Magic',
154
        'World War I' => 'War',
155
        'World War II' => 'War',
156
        'Wrestling' => 'Action',
157
    ];
158
159
    /**
160
     * @param Browser $browser
161
     * @param Registry $doctrine
162
     * @param Downloader $downloader
163
     * @param $locale
164
     */
165
    public function __construct(
166
        Browser $browser,
167
        Registry $doctrine,
168
        Downloader $downloader,
169
        $locale
170
    ) {
171
        $this->browser = $browser;
172
        $this->doctrine = $doctrine;
173
        $this->downloader = $downloader;
174
        $this->locale = $locale;
175
    }
176
177
    /**
178
     * @return string
179
     */
180
    public function getName() {
181
        return self::NAME;
182
    }
183
184
    /**
185
     * @return string
186
     */
187
    public function getTitle() {
188
        return self::TITLE;
189
    }
190
191
    /**
192
     * @return FillerForm
193
     */
194
    public function getForm()
195
    {
196
        return new FillerForm($this->browser->getHost());
0 ignored issues
show
Bug Best Practice introduced by
The return type of return new \AnimeDb\Bund...s->browser->getHost()); (AnimeDb\Bundle\AniDbFillerBundle\Form\Type\Filler) is incompatible with the return type declared by the interface AnimeDb\Bundle\CatalogBu...illerInterface::getForm of type AnimeDb\Bundle\CatalogBu...ugin\Fill\Filler\Filler.

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:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
197
    }
198
199
    /**
200
     * Build menu for plugin
201
     *
202
     * @param ItemInterface $item
203
     *
204
     * @return ItemInterface
205
     */
206
    public function buildMenu(ItemInterface $item)
207
    {
208
        return parent::buildMenu($item)
209
            ->setLinkAttribute('class', 'icon-label icon-label-plugin-anidb');
210
    }
211
212
    /**
213
     * Fill item from source
214
     *
215
     * @param array $data
216
     *
217
     * @return Item|null
218
     */
219
    public function fill(array $data)
220
    {
221
        if (empty($data['url']) || !is_string($data['url']) ||
222
            strpos($data['url'], $this->browser->getHost()) !== 0 ||
223
            !preg_match(self::REG_ITEM_ID, $data['url'], $match)
224
        ) {
225
            return null;
226
        }
227
        $body = $this->browser->get('anime', ['aid' => $match['id']]);
0 ignored issues
show
Deprecated Code introduced by
The method AnimeDb\Bundle\AniDbBrow...\Service\Browser::get() has been deprecated with message: get() is deprecated since AniDbBrowser 2.0. Use getCrawler() instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
228
229
        $item = new Item();
230
        $item->setEpisodesNumber($body->filter('episodecount')->text());
231
        $item->setDatePremiere(new \DateTime($body->filter('startdate')->text()));
232
        $item->setDateEnd(new \DateTime($body->filter('enddate')->text()));
233
        // remove links in summary
234
        $reg = '#'.preg_quote($this->browser->getHost()).'/ch\d+ \[([^\]]+)\]#';
235
        $item->setSummary(preg_replace($reg, '$1', $body->filter('description')->text()));
236
237
        // set main source
238
        $source = new Source();
239
        $source->setUrl($data['url']);
240
        $item->addSource($source);
241
242
        // add url to offsite
243
        $source = new Source();
244
        $source->setUrl($body->filter('url')->text());
245
        $item->addSource($source);
246
247
        // set complex data
248
        $this->setCover($item, $body, $match['id']);
249
        $this->setNames($item, $body);
250
        $this->setEpisodes($item, $body);
251
        $this->setType($item, $body);
252
        $this->setGenres($item, $body);
253
        return $item;
254
    }
255
256
    /**
257
     * @param Item $item
258
     * @param Crawler $body
259
     *
260
     * @return Item
261
     */
262
    public function setNames(Item $item, Crawler $body)
263
    {
264
        $titles = $body->filter('titles > title');
265
        $names = [];
266
        /* @var $title \DOMElement */
267
        foreach ($titles as $title) {
268
            $lang = substr($title->attributes->item(0)->nodeValue, 0, 2);
269
            if ($lang != 'x-') {
270
                $names[$lang][$title->getAttribute('type')] = $title->nodeValue;
271
            }
272
        }
273
274
        // set main name
275
        if (!empty($names[$this->locale])) {
276
            $item->setName($this->getNameForLocale($this->locale, $names));
277 View Code Duplication
        } elseif ($this->locale != 'en' && !empty($names['en'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
278
            $item->setName($this->getNameForLocale('en', $names));
279
        } else {
280
            $item->setName($this->getNameForLocale(array_keys($names)[0], $names));
281
        }
282
283
        // set other names
284
        $other = [];
285
        foreach ($names as $locales) {
286
            foreach ($locales as $name) {
287
                $other[] = $name;
288
            }
289
        }
290
        $other = array_unique($other);
291
        sort($other);
292
293
        foreach ($other as $name) {
294
            $item->addName((new Name())->setName($name));
295
        }
296
297
        return $item;
298
    }
299
300
    /**
301
     * @param Item $item
302
     * @param Crawler $body
303
     * @param string $id
304
     *
305
     * @return Item
306
     */
307
    public function setCover(Item $item, Crawler $body, $id)
308
    {
309
        if ($image = $body->filter('picture')->text()) {
310
            try {
311
                $image = $this->browser->getImageUrl($image);
312
                if ($path = parse_url($image, PHP_URL_PATH)) {
313
                    $ext = pathinfo($path, PATHINFO_EXTENSION);
314
                    $item->setCover(self::NAME.'/'.$id.'/cover.'.$ext);
315
                    $this->uploadImageFromUrl($image, $item);
316
                }
317
            } catch (\Exception $e) {} // error while retrieving images is not critical
318
        }
319
        return $item;
320
    }
321
322
    /**
323
     * @param Item $item
324
     * @param Crawler $body
325
     *
326
     * @return Item
327
     */
328
    public function setEpisodes(Item $item, Crawler $body)
329
    {
330
        $episodes = '';
331
        foreach ($body->filter('episodes > episode') as $episode) {
332
            $episode = new Crawler($episode);
333
            $episodes .= $episode->filter('epno')->text().'. '.$this->getEpisodeTitle($episode)."\n";
334
        }
335
        $item->setEpisodes(trim($episodes));
336
        return $item;
337
    }
338
339
    /**
340
     * @param Crawler $episode
341
     *
342
     * @return string
343
     */
344
    protected function getEpisodeTitle(Crawler $episode)
345
    {
346
        $titles = [];
347
        /* @var $title \DOMElement */
348
        foreach ($episode->filter('title') as $title) {
349
            $lang = substr($title->attributes->item(0)->nodeValue, 0, 2);
350
            if ($lang == $this->locale) {
351
                return $title->nodeValue;
352
            }
353
            if ($lang != 'x-') {
354
                $titles[$lang] = $title->nodeValue;
355
            }
356
        }
357
358
        // get EN lang or first
359
        if (!empty($titles['en'])) {
360
            return $titles['en'];
361
        } else {
362
            return array_shift($titles);
363
        }
364
    }
365
366
    /**
367
     * @param Item $item
368
     * @param Crawler $body
369
     *
370
     * @return Item
371
     */
372
    public function setType(Item $item, Crawler $body)
373
    {
374
        $rename = [
375
            'TV Series' => 'TV',
376
            'Movie' => 'Feature',
377
            'Web' => 'ONA',
378
        ];
379
        $type = $body->filter('anime > type')->text();
380
        $type = isset($rename[$type]) ? $rename[$type] : $type;
381
        return $item->setType(
382
            $this
383
                ->doctrine
384
                ->getRepository('AnimeDbCatalogBundle:Type')
385
                ->findOneBy(['name' => $type])
386
        );
387
    }
388
389
    /**
390
     * @param Item $item
391
     * @param Crawler $body
392
     *
393
     * @return Item
394
     */
395
    public function setGenres(Item $item, Crawler $body)
396
    {
397
        $repository = $this->doctrine->getRepository('AnimeDbCatalogBundle:Genre');
398
        $categories = $body->filter('categories > category > name');
399
        foreach ($categories as $category) {
400
            if (isset($this->category_to_genre[$category->nodeValue])) {
401
                $genre = $repository->findOneBy(['name' => $this->category_to_genre[$category->nodeValue]]);
402
            } else {
403
                $genre = $repository->findOneBy(['name' => $category->nodeValue]);
404
            }
405
            if ($genre instanceof Genre) {
406
                $item->addGenre($genre);
407
            }
408
        }
409
        return $item;
410
    }
411
412
    /**
413
     * @param string $url
414
     * @param EntityInterface $entity
415
     *
416
     * @return boolean
417
     */
418
    protected function uploadImageFromUrl($url, EntityInterface $entity) {
419
        return $this->downloader->image($url, $this->downloader->getRoot().$entity->getWebPath());
420
    }
421
422
    /**
423
     * @param string $locale
424
     * @param array $names
425
     *
426
     * @return string
427
     */
428 View Code Duplication
    protected function getNameForLocale($locale, & $names)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
429
    {
430
        if (isset($names[$locale]['main'])) {
431
            $name = $names[$locale]['main'];
432
            unset($names[$locale]['main']);
433
        } elseif (isset($names[$locale]['official'])) {
434
            $name = $names[$locale]['official'];
435
            unset($names[$locale]['official']);
436
        } else {
437
            $name = array_shift($names[$locale]);
438
        }
439
        return $name;
440
    }
441
442
    /**
443
     * @param string $url
444
     *
445
     * @return boolean
446
     */
447
    public function isSupportedUrl($url)
448
    {
449
        return strpos($url, $this->browser->getHost()) === 0;
450
    }
451
}
452