Completed
Push — dev ( 94beda...bb70d3 )
by Darko
06:55
created

AniDB::checkDuplicateDbEntry()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 4
dl 0
loc 3
ccs 0
cts 0
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Blacklight\db\populate;
4
5
use App\Models\AnidbEpisode;
6
use App\Models\AnidbInfo;
7
use App\Models\AnidbTitle;
8
use App\Models\Settings;
9
use Blacklight\ColorCLI;
10
use Blacklight\ReleaseImage;
11
use Illuminate\Support\Carbon;
12
13
class AniDB
14
{
15
    private const CLIENT_VERSION = 2;
16
17
    /**
18
     * Whether or not to echo message output.
19
     * @var bool
20
     */
21
    public $echooutput;
22
23
    /**
24
     * The directory to store AniDB covers.
25
     * @var string
26
     */
27
    public $imgSavePath;
28
29
    /**
30
     * The name of the nZEDb client for AniDB lookups.
31
     * @var string
32
     */
33
    private $apiKey;
34
35
    /**
36
     * Whether or not AniDB thinks our client is banned.
37
     * @var bool
38
     */
39
    private $banned;
40
41
    /**
42
     * The last unixtime a full AniDB update was run.
43
     * @var string
44
     */
45
    private $lastUpdate;
46
47
    /**
48
     * The number of days between full AniDB updates.
49
     * @var string
50
     */
51
    private $updateInterval;
52
53
    /**
54
     * @var \Blacklight\ColorCLI
55
     */
56
    protected $colorCli;
57
58
    /**
59
     * @param array $options Class instances / Echo to cli.
60
     *
61
     * @throws \Exception
62
     */
63
    public function __construct(array $options = [])
64
    {
65
        $defaults = [
66
            'Echo'     => false,
67
            'Settings' => null,
68
        ];
69
        $options += $defaults;
70
71
        $this->echooutput = ($options['Echo'] && config('nntmux.echocli'));
72
        $this->colorCli = new ColorCLI();
73
74
        $anidbupdint = Settings::settingValue('APIs.AniDB.max_update_frequency');
75
        $lastupdated = Settings::settingValue('APIs.AniDB.last_full_update');
76
77
        $this->imgSavePath = NN_COVERS.'anime'.DS;
78
        $this->apiKey = Settings::settingValue('APIs..anidbkey');
79
80
        $this->updateInterval = $anidbupdint ?? '7';
81
        $this->lastUpdate = $lastupdated ?? '0';
82
        $this->banned = false;
83
    }
84
85
    /**
86
     * Main switch that initiates AniDB table population.
87
     *
88
     * @param string     $type
89
     * @param int|string $anidbId
90
     *
91
     * @throws \Exception
92
     */
93
    public function populateTable($type = '', $anidbId = ''): void
94
    {
95
        switch ($type) {
96
            case 'full':
97
                $this->populateMainTable();
98
                break;
99
            case 'info':
100
                $this->populateInfoTable($anidbId);
101
                break;
102
        }
103
    }
104
105
    /**
106
     * @param $anidbId
107
     *
108
     * @return array|false
109
     */
110
    private function getAniDbAPI($anidbId)
111
    {
112
        $timestamp = Settings::settingValue('APIs.AniDB.banned') + 90000;
113
        if ($timestamp > time()) {
114
            echo 'Banned from AniDB lookups until '.date('Y-m-d H:i:s', $timestamp).PHP_EOL;
115
116
            return false;
117
        }
118
        $apiresponse = $this->getAniDbResponse($anidbId);
119
120
        $AniDBAPIArray = [];
121
122
        if ($apiresponse === false) {
0 ignored issues
show
introduced by
The condition $apiresponse === false is always false.
Loading history...
123
            echo 'AniDB: Error getting response.'.PHP_EOL;
124
        } elseif (preg_match('/\<error\>Banned\<\/error\>/', $apiresponse)) {
125
            $this->banned = true;
126
            Settings::query()->where(['section' => 'APIs', 'subsection' => 'AniDB', 'name' => 'banned'])->update(['value' => time()]);
127
        } elseif (preg_match('/\<error\>Anime not found\<\/error\>/', $apiresponse)) {
128
            echo "AniDB   : Anime not yet on site. Remove until next update.\n";
129
        } elseif ($AniDBAPIXML = new \SimpleXMLElement($apiresponse)) {
130
            $AniDBAPIArray['similar'] = $this->processAPIResponseElement($AniDBAPIXML->similaranime, 'anime', false);
131
            $AniDBAPIArray['related'] = $this->processAPIResponseElement($AniDBAPIXML->relatedanime, 'anime', false);
132
            $AniDBAPIArray['creators'] = $this->processAPIResponseElement($AniDBAPIXML->creators, null, false);
133
            $AniDBAPIArray['characters'] = $this->processAPIResponseElement($AniDBAPIXML->characters, null, true);
134
            $AniDBAPIArray['categories'] = $this->processAPIResponseElement($AniDBAPIXML->categories, null, true);
135
136
            $episodeArray = [];
137
            if ($AniDBAPIXML->episodes && $AniDBAPIXML->episodes->episode[0]->attributes()) {
138
                $i = 1;
139
                foreach ($AniDBAPIXML->episodes->episode as $episode) {
140
                    $titleArray = [];
141
142
                    $episodeArray[$i]['episode_id'] = (int) $episode->attributes()->id;
143
                    $episodeArray[$i]['episode_no'] = (int) $episode->epno;
144
                    $episodeArray[$i]['airdate'] = (string) $episode->airdate;
145
146
                    if (! empty($episode->title)) {
147
                        foreach ($episode->title as $title) {
148
                            $xmlAttribs = $title->attributes('xml', true);
149
                            // only english, x-jat imploded episode titles for now
150
                            if (\in_array($xmlAttribs->lang, ['en', 'x-jat'], false)) {
151
                                $titleArray[] = $title[0];
152
                            }
153
                        }
154
                    }
155
156
                    $episodeArray[$i]['episode_title'] = empty($titleArray) ? '' : implode(', ', $titleArray);
157
                    $i++;
158
                }
159
            }
160
161
            //start and end date come from AniDB API as date strings -- no manipulation needed
162
            $AniDBAPIArray['startdate'] = $AniDBAPIXML->startdate ?? '0000-00-00';
163
            $AniDBAPIArray['enddate'] = $AniDBAPIXML->enddate ?? '0000-00-00';
164
165
            if (isset($AniDBAPIXML->ratings->permanent)) {
166
                $AniDBAPIArray['rating'] = $AniDBAPIXML->ratings->permanent;
167
            } else {
168
                $AniDBAPIArray['rating'] = $AniDBAPIXML->ratings->temporary ?? $AniDBAPIArray['rating'] = '';
169
            }
170
171
            $AniDBAPIArray += [
172
                'type'        => isset($AniDBAPIXML->type[0]) ? (string) $AniDBAPIXML->type : '',
173
                'description' => isset($AniDBAPIXML->description) ? (string) $AniDBAPIXML->description : '',
174
                'picture'     => isset($AniDBAPIXML->picture[0]) ? (string) $AniDBAPIXML->picture : '',
175
                'epsarr'      => $episodeArray,
176
            ];
177
178
            return $AniDBAPIArray;
179
        }
180
181
        return false;
182
    }
183
184
    /**
185
     * @param \SimpleXMLElement $element
186
     * @param string            $property
187
     *
188
     * @param bool              $children
189
     *
190
     * @return string
191
     */
192
    private function processAPIResponseElement(\SimpleXMLElement $element, $property = null, $children = false): string
193
    {
194
        $property = $property ?? 'name';
195
        $temp = '';
196
197
        if (\is_object($element) && ! empty($element)) {
198
            $result = $children === true ? $element->children() : $element;
199
            foreach ($result as $entry) {
200
                $temp .= (string) $entry->$property.', ';
201
            }
202
        }
203
204
        return empty($temp) ? '' : substr($temp, 0, -2);
205
    }
206
207
    /**
208
     * Requests and returns the API data from AniDB.
209
     *
210
     * @param $anidbId
211
     * @return string
212
     */
213
    private function getAniDbResponse($anidbId): string
214
    {
215
        $curlString = sprintf(
216
            'http://api.anidb.net:9001/httpapi?request=anime&client=%s&clientver=%d&protover=1&aid=%d',
217
            $this->apiKey,
218
            self::CLIENT_VERSION,
219
            $anidbId
220
        );
221
222
        $ch = curl_init($curlString);
223
224
        $curlOpts = [
225
            CURLOPT_RETURNTRANSFER => 1,
226
            CURLOPT_HEADER         => 0,
227
            CURLOPT_FAILONERROR    => 1,
228
            CURLOPT_ENCODING       => 'gzip',
229
        ];
230
231
        curl_setopt_array($ch, $curlOpts);
0 ignored issues
show
Bug introduced by
It seems like $ch can also be of type false; however, parameter $ch of curl_setopt_array() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

231
        curl_setopt_array(/** @scrutinizer ignore-type */ $ch, $curlOpts);
Loading history...
232
        $apiresponse = curl_exec($ch);
0 ignored issues
show
Bug introduced by
It seems like $ch can also be of type false; however, parameter $ch of curl_exec() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

232
        $apiresponse = curl_exec(/** @scrutinizer ignore-type */ $ch);
Loading history...
233
        curl_close($ch);
0 ignored issues
show
Bug introduced by
It seems like $ch can also be of type false; however, parameter $ch of curl_close() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

233
        curl_close(/** @scrutinizer ignore-type */ $ch);
Loading history...
234
235
        return $apiresponse;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $apiresponse could return the type boolean which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
236
    }
237
238
    /**
239
     * Inserts new anime info from AniDB to anidb table.
240
     *
241
     * @param int    $id    The AniDB ID to be inserted
242
     * @param string $type  The title type
243
     * @param string $lang  The title language
244
     * @param string $title The title of the Anime
245
     */
246
    private function insertAniDb($id, $type, $lang, $title): void
247
    {
248
        $check = AnidbTitle::query()->where(['anidbid' => $id, 'type' => $type, 'lang' => $lang, 'title' => $title])->first();
249
250
        if ($check === null) {
251
            AnidbTitle::insertOrIgnore(['anidbid' => $id, 'type' => $type, 'lang' => $lang, 'title' => $title]);
252
        } else {
253
            $this->colorCli->warning("Duplicate: $id");
254
        }
255
    }
256
257
    /**
258
     * Inserts new anime info from AniDB to anidb table.
259
     *
260
     * @param array $AniDBInfoArray
261
     *
262
     * @return string
263
     */
264
    private function insertAniDBInfoEps(array $AniDBInfoArray = [], $anidbId): string
265
    {
266
        AnidbInfo::query()
267
            ->insert(
268
                [
269
                    'anidbid' => $anidbId,
270
                    'type' => $AniDBInfoArray['type'],
271
                    'startdate' => $AniDBInfoArray['startdate'],
272
                    'enddate' => $AniDBInfoArray['enddate'],
273
                    'related' => $AniDBInfoArray['enddate'],
274
                    'similar' => $AniDBInfoArray['similar'],
275
                    'creators' => $AniDBInfoArray['creators'],
276
                    'description' => $AniDBInfoArray['description'],
277
                    'rating' => $AniDBInfoArray['rating'],
278
                    'picture' => $AniDBInfoArray['picture'],
279
                    'categories' => $AniDBInfoArray['categories'],
280
                    'characters' => $AniDBInfoArray['characters'],
281
                    'updated' => now(),
282
                ]
283
            );
284
        if (! empty($AniDBInfoArray['epsarr'])) {
285
            $this->insertAniDBEpisodes($AniDBInfoArray['epsarr'], $anidbId);
286
        }
287
288
        return $AniDBInfoArray['picture'];
289
    }
290
291
    /**
292
     * Inserts new anime info from AniDB to anidb table.
293
     *
294
     * @param array $episodeArr
295
     */
296
    private function insertAniDBEpisodes(array $episodeArr = [], $anidbId): void
297
    {
298
        if (! empty($episodeArr)) {
299
            foreach ($episodeArr as $episode) {
300
                AnidbEpisode::insertOrIgnore(
301
                    [
302
                        'anidbid' => $anidbId,
303
                        'episodeid' => $episode['episode_id'],
304
                        'episode_no' => $episode['episode_no'],
305
                        'episode_title' => $episode['episode_title'],
306
                        'airdate' => $episode['airdate'],
307
                    ]
308
                );
309
            }
310
        }
311
    }
312
313
    /**
314
     *  Grabs AniDB Full Dump XML and inserts it into anidb table.
315
     */
316
    private function populateMainTable()
317
    {
318
        $lastUpdate = Carbon::createFromTimestamp($this->lastUpdate);
0 ignored issues
show
Bug introduced by
$this->lastUpdate of type string is incompatible with the type integer expected by parameter $timestamp of Carbon\Carbon::createFromTimestamp(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

318
        $lastUpdate = Carbon::createFromTimestamp(/** @scrutinizer ignore-type */ $this->lastUpdate);
Loading history...
319
        $current = now();
320
321
        if ($current->diff($lastUpdate)->format('%d') > $this->updateInterval) {
322
            if ($this->echooutput) {
323
                $this->colorCli->header('Updating anime titles by grabbing full data AniDB dump.');
324
            }
325
326
            $animetitles = new \SimpleXMLElement('compress.zlib://http://anidb.net/api/anime-titles.xml.gz', null, true);
327
328
            //Even if the update process fails,
329
            //we must mark the last update time or risk ban
330
            $this->setLastUpdated();
331
332
            if ($animetitles instanceof \Traversable) {
0 ignored issues
show
introduced by
$animetitles is always a sub-type of Traversable.
Loading history...
333
                $count = $animetitles->count();
334
                if ($this->echooutput) {
335
                    $this->colorCli->header(
336
                        'Total of '.number_format($count).' titles to add.'.PHP_EOL
337
                    );
338
                }
339
340
                foreach ($animetitles as $anime) {
341
                    echo "Remaining: $count  \r";
342
                    foreach ($anime->title as $title) {
343
                        $xmlAttribs = $title->attributes('xml', true);
344
                        $this->insertAniDb(
345
                            (string) $anime['aid'],
0 ignored issues
show
Bug introduced by
(string)$anime['aid'] of type string is incompatible with the type integer expected by parameter $id of Blacklight\db\populate\AniDB::insertAniDb(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

345
                            /** @scrutinizer ignore-type */ (string) $anime['aid'],
Loading history...
346
                            (string) $title['type'],
347
                            (string) $xmlAttribs->lang,
348
                            (string) $title[0]
349
                        );
350
                        $this->colorCli->primary(
351
                            sprintf(
352
                                'Inserting: %d, %s, %s, %s',
353
                                $anime['aid'],
354
                                $title['type'],
355
                                $xmlAttribs->lang,
356
                                $title[0]
357
                            )
358
                        );
359
                    }
360
                    $count--;
361
                }
362
            } else {
363
                echo PHP_EOL.
364
                    $this->colorCli->error('Error retrieving XML data from AniDB. Please try again later.').
365
                    PHP_EOL;
366
            }
367
        } else {
368
            $this->colorCli->info(
369
                    'AniDB has been updated within the past '.$this->updateInterval.' days. '.
370
                    'Either set this value lower in Site Edit (at your own risk of being banned) or try again later.'
371
            );
372
        }
373
    }
374
375
    /**
376
     * Directs flow for populating the AniDB Info/Episodes table.
377
     *
378
     * @param string $anidbId
379
     *
380
     * @throws \Exception
381
     */
382
    private function populateInfoTable($anidbId = '')
383
    {
384
        if (empty($anidbId)) {
385
            $anidbIds = AnidbTitle::query()
386
                ->selectRaw('DISTINCT anidb_titles.anidbid')
387
                ->leftJoin('anidb_info as ai', 'ai.anidbid', '=', 'anidb_titles.anidbid')
388
                ->whereNull('ai.updated')
389
                ->get();
390
391
            foreach ($anidbIds as $anidb) {
392
                $AniDBAPIArray = $this->getAniDbAPI($anidb['anidbid']);
393
394
                if ($this->banned === true) {
395
                    $this->colorCli->error(
396
                            'AniDB Banned, import will fail, please wait 24 hours before retrying.'
397
                        );
398
                    exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
399
                }
400
401
                if ($AniDBAPIArray === false && $this->echooutput) {
402
                    $this->colorCli->info(
403
                            'Anime ID: '.$anidb['anidbid'].' not available for update yet.'
404
                        );
405
                } else {
406
                    $this->updateAniChildTables($AniDBAPIArray, $anidb['anidbid']);
0 ignored issues
show
Bug introduced by
It seems like $AniDBAPIArray can also be of type false; however, parameter $AniDBInfoArray of Blacklight\db\populate\A...:updateAniChildTables() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

406
                    $this->updateAniChildTables(/** @scrutinizer ignore-type */ $AniDBAPIArray, $anidb['anidbid']);
Loading history...
407
                }
408
                sleep(random_int(120, 240));
409
            }
410
        } else {
411
            $AniDBAPIArray = $this->getAniDbAPI($anidbId);
412
413
            if ($this->banned === true) {
414
                $this->colorCli->error(
415
                        'AniDB Banned, import will fail, please wait 24 hours before retrying.'
416
                    );
417
                exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
418
            }
419
420
            if ($AniDBAPIArray === false && $this->echooutput) {
421
                $this->colorCli->info(
422
                        'Anime ID: '.$anidbId.' not available for update yet.'
423
                    );
424
            } else {
425
                $this->updateAniChildTables($AniDBAPIArray, $anidbId);
426
            }
427
        }
428
    }
429
430
    /**
431
     * Sets the database time for last full AniDB update.
432
     */
433
    private function setLastUpdated(): void
434
    {
435
        Settings::query()->where(['section' => 'APIs', 'subsection' => 'AniDB', 'name' => 'last_full_update'])->update(['value' => time()]);
436
    }
437
438
    /**
439
     * Updates existing anime info in anidb info/episodes tables.
440
     *
441
     * @param array $AniDBInfoArray
442
     *
443
     * @return string
444
     */
445
    private function updateAniDBInfoEps(array $AniDBInfoArray = [], $anidbId): string
446
    {
447
        AnidbInfo::query()
448
            ->where('anidbid', $anidbId)
449
            ->update(
450
                [
451
                    'type' => $AniDBInfoArray['type'],
452
                    'startdate' => $AniDBInfoArray['startdate'],
453
                    'enddate' => $AniDBInfoArray['enddate'],
454
                    'related' => $AniDBInfoArray['enddate'],
455
                    'similar' => $AniDBInfoArray['similar'],
456
                    'creators' => $AniDBInfoArray['creators'],
457
                    'description' => $AniDBInfoArray['description'],
458
                    'rating' => $AniDBInfoArray['rating'],
459
                    'picture' => $AniDBInfoArray['picture'],
460
                    'categories' => $AniDBInfoArray['categories'],
461
                    'characters' => $AniDBInfoArray['characters'],
462
                    'updated' => now(),
463
                ]
464
            );
465
        if (! empty($AniDBInfoArray['epsarr'])) {
466
            $this->insertAniDBEpisodes($AniDBInfoArray['epsarr'], $anidbId);
467
        }
468
469
        return $AniDBInfoArray['picture'];
470
    }
471
472
    /**
473
     * Directs flow for updating child AniDB tables.
474
     *
475
     * @param array $AniDBInfoArray
476
     * @param       $anidbId
477
     */
478
    private function updateAniChildTables(array $AniDBInfoArray = [], $anidbId): void
479
    {
480
        $check = AnidbInfo::query()->where('anidbid', $anidbId)->first(['anidbid']);
481
482
        if ($check === null) {
483
            $picture = $this->insertAniDBInfoEps($AniDBInfoArray, $anidbId);
484
        } else {
485
            $picture = $this->updateAniDBInfoEps($AniDBInfoArray, $anidbId);
486
        }
487
488
        if (! empty($picture) && ! file_exists($this->imgSavePath.$anidbId.'.jpg')) {
489
            (new ReleaseImage())->saveImage(
490
                $anidbId,
491
                'http://img7.anidb.net/pics/anime/'.$picture,
492
                $this->imgSavePath
493
            );
494
        }
495
    }
496
}
497