Passed
Push — master ( 392926...b4a664 )
by Darko
13:01
created

MovieService::searchOMDbAPI()   B

Complexity

Conditions 9
Paths 13

Size

Total Lines 30
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 19
c 1
b 0
f 0
dl 0
loc 30
rs 8.0555
cc 9
nc 13
nop 1
1
<?php
2
3
namespace App\Services;
4
5
use aharen\OMDbAPI;
6
use App\Models\Category;
7
use App\Models\MovieInfo;
8
use App\Models\Release;
9
use App\Models\Settings;
10
use App\Services\TvProcessing\Providers\TraktProvider;
11
use Blacklight\ColorCLI;
12
use Blacklight\ReleaseImage;
13
use GuzzleHttp\Client;
14
use GuzzleHttp\Exception\GuzzleException;
15
use Illuminate\Support\Carbon;
16
use Illuminate\Support\Facades\Cache;
17
use Illuminate\Support\Facades\File;
18
use Illuminate\Support\Facades\Log;
19
use Illuminate\Support\Str;
20
21
/**
22
 * Service class for movie data fetching and processing.
23
 */
24
class MovieService
25
{
26
    protected const MATCH_PERCENT = 75;
27
28
    protected const YEAR_MATCH_PERCENT = 80;
29
30
    protected string $currentTitle = '';
31
32
    protected string $currentYear = '';
33
34
    protected string $currentRelID = '';
35
36
    protected string $showPasswords;
37
38
    protected ReleaseImage $releaseImage;
39
40
    protected Client $client;
41
42
    protected string $lookuplanguage;
43
44
    public FanartTvService $fanart;
45
46
    public ?string $fanartapikey;
47
48
    public ?string $omdbapikey;
49
50
    public bool $imdburl;
51
52
    public int $movieqty;
53
54
    public bool $echooutput;
55
56
    public string $imgSavePath;
57
58
    public string $service;
59
60
    public ?TraktProvider $traktTv = null;
61
62
    public ?OMDbAPI $omdbApi = null;
63
64
    protected ?string $traktcheck;
65
66
    protected ColorCLI $colorCli;
67
68
    /**
69
     * @throws \Exception
70
     */
71
    public function __construct()
72
    {
73
        $this->releaseImage = new ReleaseImage;
74
        $this->colorCli = new ColorCLI;
75
        $this->traktcheck = config('nntmux_api.trakttv_api_key');
76
        if ($this->traktcheck !== null) {
77
            $this->traktTv = new TraktProvider();
78
        }
79
        $this->client = new Client;
80
        $this->fanartapikey = config('nntmux_api.fanarttv_api_key');
81
        $this->fanart = new FanartTvService($this->fanartapikey);
82
        $this->omdbapikey = config('nntmux_api.omdb_api_key');
83
        if ($this->omdbapikey !== null) {
84
            $this->omdbApi = new OMDbAPI($this->omdbapikey);
85
        }
86
87
        $this->lookuplanguage = Settings::settingValue('imdblanguage') !== '' ? (string) Settings::settingValue('imdblanguage') : 'en';
88
        $cacheDir = storage_path('framework/cache/imdb_cache');
89
        if (! File::isDirectory($cacheDir)) {
90
            File::makeDirectory($cacheDir, 0777, false, true);
91
        }
92
93
        $this->imdburl = (int) Settings::settingValue('imdburl') !== 0;
94
        $this->movieqty = Settings::settingValue('maximdbprocessed') !== '' ? (int) Settings::settingValue('maximdbprocessed') : 100;
95
        $this->showPasswords = app(\App\Services\Releases\ReleaseBrowseService::class)->showPasswords();
96
97
        $this->echooutput = config('nntmux.echocli');
98
        $this->imgSavePath = storage_path('covers/movies/');
99
        $this->service = '';
100
    }
101
102
    /**
103
     * Get movie info by IMDB ID.
104
     */
105
    public function getMovieInfo(string $imdbId): ?MovieInfo
106
    {
107
        return MovieInfo::query()->where('imdbid', $imdbId)->first();
108
    }
109
110
    /**
111
     * Get trailer using IMDB Id.
112
     *
113
     * @throws \Exception
114
     * @throws GuzzleException
115
     */
116
    public function getTrailer(int $imdbId): string|false
117
    {
118
        $trailer = MovieInfo::query()->where('imdbid', $imdbId)->where('trailer', '<>', '')->first(['trailer']);
119
        if ($trailer !== null) {
120
            return $trailer['trailer'];
121
        }
122
123
        if ($this->traktcheck !== null) {
124
            $data = $this->traktTv->client->getMovieSummary('tt'.$imdbId, 'full');
125
            if (($data !== false) && ! empty($data['trailer'])) {
126
                return $data['trailer'];
127
            }
128
        }
129
130
        $trailer = imdb_trailers($imdbId);
131
        if ($trailer) {
132
            MovieInfo::query()->where('imdbid', $imdbId)->update(['trailer' => $trailer]);
133
134
            return $trailer;
135
        }
136
137
        return false;
138
    }
139
140
    /**
141
     * Parse trakt info, insert into DB.
142
     */
143
    public function parseTraktTv(array &$data): mixed
144
    {
145
        if (empty($data['ids']['imdb'])) {
146
            return false;
147
        }
148
149
        if (! empty($data['trailer'])) {
150
            $data['trailer'] = str_ireplace(
151
                ['watch?v=', 'http://'],
152
                ['embed/', 'https://'],
153
                $data['trailer']
154
            );
155
        }
156
        $imdbId = (str_starts_with($data['ids']['imdb'], 'tt')) ? substr($data['ids']['imdb'], 2) : $data['ids']['imdb'];
157
        $cover = 0;
158
        if (File::isFile($this->imgSavePath.$imdbId.'-cover.jpg')) {
159
            $cover = 1;
160
        }
161
162
        return $this->update([
163
            'genre' => implode(', ', $data['genres']),
164
            'imdbid' => $this->checkTraktValue($imdbId),
165
            'language' => $this->checkTraktValue($data['language']),
166
            'plot' => $this->checkTraktValue($data['overview']),
167
            'rating' => $this->checkTraktValue($data['rating']),
168
            'tagline' => $this->checkTraktValue($data['tagline']),
169
            'title' => $this->checkTraktValue($data['title']),
170
            'tmdbid' => $this->checkTraktValue($data['ids']['tmdb']),
171
            'traktid' => $this->checkTraktValue($data['ids']['trakt']),
172
            'trailer' => $this->checkTraktValue($data['trailer']),
173
            'cover' => $cover,
174
            'year' => $this->checkTraktValue($data['year']),
175
        ]);
176
    }
177
178
    private function checkTraktValue(mixed $value): mixed
179
    {
180
        if (\is_array($value) && ! empty($value)) {
181
            $temp = '';
182
            foreach ($value as $val) {
183
                if (! is_array($val) && ! is_object($val)) {
184
                    $temp .= $val;
185
                }
186
            }
187
            $value = $temp;
188
        }
189
190
        return ! empty($value) ? $value : '';
191
    }
192
193
    /**
194
     * Get array of column keys, for inserting / updating.
195
     */
196
    public function getColumnKeys(): array
197
    {
198
        return [
199
            'actors', 'backdrop', 'cover', 'director', 'genre', 'imdbid', 'language',
200
            'plot', 'rating', 'rtrating', 'tagline', 'title', 'tmdbid', 'traktid', 'trailer', 'type', 'year',
201
        ];
202
    }
203
204
    /**
205
     * Choose the first non-empty variable from up to five inputs.
206
     */
207
    protected function setVariables(string|array $variable1, string|array $variable2, string|array $variable3, string|array $variable4, string|array $variable5 = ''): array|string
208
    {
209
        if (! empty($variable1)) {
210
            return $variable1;
211
        }
212
        if (! empty($variable2)) {
213
            return $variable2;
214
        }
215
        if (! empty($variable3)) {
216
            return $variable3;
217
        }
218
        if (! empty($variable4)) {
219
            return $variable4;
220
        }
221
        if (! empty($variable5)) {
222
            return $variable5;
223
        }
224
225
        return '';
226
    }
227
228
    /**
229
     * Update movie on movie-edit page.
230
     */
231
    public function update(array $values): bool
232
    {
233
        if (! count($values)) {
234
            return false;
235
        }
236
237
        $query = [];
238
        $onDuplicateKey = ['created_at' => now()];
239
        $found = 0;
240
        foreach ($values as $key => $value) {
241
            if (! empty($value)) {
242
                $found++;
243
                if (\in_array($key, ['genre', 'language'], false)) {
244
                    $value = substr($value, 0, 64);
245
                }
246
                $query += [$key => $value];
247
                $onDuplicateKey += [$key => $value];
248
            }
249
        }
250
        if (! $found) {
251
            return false;
252
        }
253
        foreach ($query as $key => $value) {
254
            $query[$key] = rtrim($value, ', ');
255
        }
256
257
        MovieInfo::upsert($query, ['imdbid'], $onDuplicateKey);
258
259
        // Always attempt to fetch a missing cover if imdbid present and cover not provided.
260
        if (! empty($query['imdbid'])) {
261
            $imdbIdForCover = $query['imdbid'];
262
            $coverProvided = array_key_exists('cover', $values) && ! empty($values['cover']);
263
            if (! $coverProvided && ! $this->hasCover($imdbIdForCover)) {
264
                if ($this->fetchAndSaveCoverOnly($imdbIdForCover)) {
265
                    MovieInfo::query()->where('imdbid', $imdbIdForCover)->update(['cover' => 1]);
266
                }
267
            }
268
        }
269
270
        return true;
271
    }
272
273
    /**
274
     * Fetch IMDB/TMDB/TRAKT/OMDB/iTunes info for the movie.
275
     *
276
     * @throws \Exception
277
     */
278
    public function updateMovieInfo(string $imdbId): bool
279
    {
280
        if ($this->echooutput && $this->service !== '') {
281
            $this->colorCli->primary('Fetching IMDB info from TMDB/IMDB/Trakt/OMDB/iTunes using IMDB id: '.$imdbId);
282
        }
283
284
        // Check TMDB for IMDB info.
285
        $tmdb = $this->fetchTMDBProperties($imdbId);
286
287
        // Check IMDB for movie info.
288
        $imdb = $this->fetchIMDBProperties($imdbId);
289
290
        // Check TRAKT for movie info
291
        $trakt = $this->fetchTraktTVProperties($imdbId);
292
293
        // Check OMDb for movie info
294
        $omdb = $this->fetchOmdbAPIProperties($imdbId);
295
296
        if (! $imdb && ! $tmdb && ! $trakt && ! $omdb) {
0 ignored issues
show
introduced by
The condition $imdb is always false.
Loading history...
297
            return false;
298
        }
299
300
        // Check FanArt.tv for cover and background images.
301
        $fanart = $this->fetchFanartTVProperties($imdbId);
302
303
        $mov = [];
304
305
        $mov['cover'] = $mov['backdrop'] = $mov['banner'] = 0;
306
        $mov['type'] = $mov['director'] = $mov['actors'] = $mov['language'] = '';
307
308
        $mov['imdbid'] = $imdbId;
309
        $mov['tmdbid'] = (! isset($tmdb['tmdbid']) || $tmdb['tmdbid'] === '') ? 0 : $tmdb['tmdbid'];
310
        $mov['traktid'] = (! isset($trakt['id']) || $trakt['id'] === '') ? 0 : $trakt['id'];
311
312
        // Prefer Fanart.tv cover over TMDB,TMDB over IMDB,IMDB over OMDB and OMDB over iTunes.
313
        if (! empty($fanart['cover'])) {
314
            try {
315
                $mov['cover'] = $this->releaseImage->saveImage($imdbId.'-cover', $fanart['cover'], $this->imgSavePath);
316
                if ($mov['cover'] === 0) {
317
                    Log::warning('Failed to save FanartTV cover for '.$imdbId.' from URL: '.$fanart['cover']);
318
                }
319
            } catch (\Throwable $e) {
320
                Log::error('Error saving FanartTV cover for '.$imdbId.': '.$e->getMessage());
321
                $mov['cover'] = 0;
322
            }
323
        }
324
325
        if ($mov['cover'] === 0 && ! empty($tmdb['cover'])) {
326
            try {
327
                $mov['cover'] = $this->releaseImage->saveImage($imdbId.'-cover', $tmdb['cover'], $this->imgSavePath);
328
                if ($mov['cover'] === 0) {
329
                    Log::warning('Failed to save TMDB cover for '.$imdbId.' from URL: '.$tmdb['cover']);
330
                }
331
            } catch (\Throwable $e) {
332
                Log::error('Error saving TMDB cover for '.$imdbId.': '.$e->getMessage());
333
                $mov['cover'] = 0;
334
            }
335
        }
336
337
        if ($mov['cover'] === 0 && ! empty($imdb['cover'])) {
338
            try {
339
                $mov['cover'] = $this->releaseImage->saveImage($imdbId.'-cover', $imdb['cover'], $this->imgSavePath);
340
                if ($mov['cover'] === 0) {
341
                    Log::warning('Failed to save IMDB cover for '.$imdbId.' from URL: '.$imdb['cover']);
342
                }
343
            } catch (\Throwable $e) {
344
                Log::error('Error saving IMDB cover for '.$imdbId.': '.$e->getMessage());
345
                $mov['cover'] = 0;
346
            }
347
        }
348
349
        if ($mov['cover'] === 0 && ! empty($omdb['cover'])) {
350
            try {
351
                $mov['cover'] = $this->releaseImage->saveImage($imdbId.'-cover', $omdb['cover'], $this->imgSavePath);
352
                if ($mov['cover'] === 0) {
353
                    Log::warning('Failed to save OMDB cover for '.$imdbId.' from URL: '.$omdb['cover']);
354
                }
355
            } catch (\Throwable $e) {
356
                Log::error('Error saving OMDB cover for '.$imdbId.': '.$e->getMessage());
357
                $mov['cover'] = 0;
358
            }
359
        }
360
361
        // Backdrops.
362
        if (! empty($fanart['backdrop'])) {
363
            try {
364
                $mov['backdrop'] = $this->releaseImage->saveImage($imdbId.'-backdrop', $fanart['backdrop'], $this->imgSavePath, 1920, 1024);
365
            } catch (\Throwable $e) {
366
                Log::warning('Error saving FanartTV backdrop for '.$imdbId.': '.$e->getMessage());
367
                $mov['backdrop'] = 0;
368
            }
369
        }
370
371
        if ($mov['backdrop'] === 0 && ! empty($tmdb['backdrop'])) {
372
            try {
373
                $mov['backdrop'] = $this->releaseImage->saveImage($imdbId.'-backdrop', $tmdb['backdrop'], $this->imgSavePath, 1920, 1024);
374
            } catch (\Throwable $e) {
375
                Log::warning('Error saving TMDB backdrop for '.$imdbId.': '.$e->getMessage());
376
                $mov['backdrop'] = 0;
377
            }
378
        }
379
380
        // Banner
381
        if (! empty($fanart['banner'])) {
382
            try {
383
                $mov['banner'] = $this->releaseImage->saveImage($imdbId.'-banner', $fanart['banner'], $this->imgSavePath);
384
            } catch (\Throwable $e) {
385
                Log::warning('Error saving FanartTV banner for '.$imdbId.': '.$e->getMessage());
386
                $mov['banner'] = 0;
387
            }
388
        }
389
390
        // RottenTomatoes rating from OmdbAPI
391
        if ($omdb !== false && ! empty($omdb['rtRating'])) {
392
            $mov['rtrating'] = $omdb['rtRating'];
393
        }
394
395
        $mov['title'] = $this->setVariables($imdb['title'] ?? '', $tmdb['title'] ?? '', $trakt['title'] ?? '', $omdb['title'] ?? '');
396
        $mov['rating'] = $this->setVariables($imdb['rating'] ?? '', $tmdb['rating'] ?? '', $trakt['rating'] ?? '', $omdb['rating'] ?? '');
397
        $mov['plot'] = $this->setVariables($imdb['plot'] ?? '', $tmdb['plot'] ?? '', $trakt['overview'] ?? '', $omdb['plot'] ?? '');
398
        $mov['tagline'] = $this->setVariables($imdb['tagline'] ?? '', $tmdb['tagline'] ?? '', $trakt['tagline'] ?? '', $omdb['tagline'] ?? '');
399
        $mov['year'] = $this->setVariables($imdb['year'] ?? '', $tmdb['year'] ?? '', $trakt['year'] ?? '', $omdb['year'] ?? '');
400
        $mov['genre'] = $this->setVariables($imdb['genre'] ?? '', $tmdb['genre'] ?? '', $trakt['genres'] ?? '', $omdb['genre'] ?? '');
401
402
        if (! empty($imdb['type'])) {
403
            $mov['type'] = $imdb['type'];
404
        }
405
406
        if (! empty($imdb['director'])) {
407
            $mov['director'] = \is_array($imdb['director']) ? implode(', ', array_unique($imdb['director'])) : $imdb['director'];
408
        } elseif (! empty($omdb['director'])) {
409
            $mov['director'] = \is_array($omdb['director']) ? implode(', ', array_unique($omdb['director'])) : $omdb['director'];
410
        } elseif (! empty($tmdb['director'])) {
411
            $mov['director'] = \is_array($tmdb['director']) ? implode(', ', array_unique($tmdb['director'])) : $tmdb['director'];
412
        }
413
414
        if (! empty($imdb['actors'])) {
415
            $mov['actors'] = \is_array($imdb['actors']) ? implode(', ', array_unique($imdb['actors'])) : $imdb['actors'];
416
        } elseif (! empty($omdb['actors'])) {
417
            $mov['actors'] = \is_array($omdb['actors']) ? implode(', ', array_unique($omdb['actors'])) : $omdb['actors'];
418
        } elseif (! empty($tmdb['actors'])) {
419
            $mov['actors'] = \is_array($tmdb['actors']) ? implode(', ', array_unique($tmdb['actors'])) : $tmdb['actors'];
420
        }
421
422
        if (! empty($imdb['language'])) {
423
            $mov['language'] = \is_array($imdb['language']) ? implode(', ', array_unique($imdb['language'])) : $imdb['language'];
424
        } elseif (! empty($omdb['language']) && ! is_bool($omdb['language'])) {
425
            $mov['language'] = \is_array($omdb['language']) ? implode(', ', array_unique($omdb['language'])) : $omdb['language'];
426
        }
427
428
        if (\is_array($mov['genre'])) {
429
            $mov['genre'] = implode(', ', array_unique($mov['genre']));
430
        }
431
432
        if (\is_array($mov['type'])) {
433
            $mov['type'] = implode(', ', array_unique($mov['type']));
434
        }
435
436
        $mov['title'] = html_entity_decode($mov['title'], ENT_QUOTES, 'UTF-8');
437
438
        $mov['title'] = str_replace(['/', '\\'], '', $mov['title']);
439
        $movieID = $this->update([
440
            'actors' => html_entity_decode($mov['actors'], ENT_QUOTES, 'UTF-8'),
441
            'backdrop' => $mov['backdrop'],
442
            'cover' => $mov['cover'],
443
            'director' => html_entity_decode($mov['director'], ENT_QUOTES, 'UTF-8'),
444
            'genre' => html_entity_decode($mov['genre'], ENT_QUOTES, 'UTF-8'),
445
            'imdbid' => $mov['imdbid'],
446
            'language' => html_entity_decode($mov['language'], ENT_QUOTES, 'UTF-8'),
447
            'plot' => html_entity_decode(preg_replace('/\s+See full summary »/u', ' ', $mov['plot']), ENT_QUOTES, 'UTF-8'),
448
            'rating' => round((int) $mov['rating'], 1),
449
            'rtrating' => $mov['rtrating'] ?? 'N/A',
450
            'tagline' => html_entity_decode($mov['tagline'], ENT_QUOTES, 'UTF-8'),
451
            'title' => $mov['title'],
452
            'tmdbid' => $mov['tmdbid'],
453
            'traktid' => $mov['traktid'],
454
            'type' => html_entity_decode(ucwords(preg_replace('/[._]/', ' ', $mov['type'])), ENT_QUOTES, 'UTF-8'),
455
            'year' => $mov['year'],
456
        ]);
457
458
        // After updating, if cover flag is still 0 but file now exists (race condition), update DB.
459
        if ($mov['cover'] === 0 && $this->hasCover($imdbId)) {
460
            MovieInfo::query()->where('imdbid', $imdbId)->update(['cover' => 1]);
461
        }
462
463
        if ($this->echooutput && $this->service !== '') {
464
            PHP_EOL.$this->colorCli->headerOver('Added/updated movie: ').
0 ignored issues
show
Bug introduced by
Are you sure $this->colorCli->headerO...Added/updated movie: ') of type void can be used in concatenation? ( Ignorable by Annotation )

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

464
            PHP_EOL./** @scrutinizer ignore-type */ $this->colorCli->headerOver('Added/updated movie: ').
Loading history...
Bug introduced by
Are you sure the usage of $this->colorCli->headerO...Added/updated movie: ') targeting Blacklight\ColorCLI::headerOver() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
465
            $this->colorCli->primary(
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->colorCli->primary...) - ' . $mov['imdbid']) targeting Blacklight\ColorCLI::primary() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Bug introduced by
Are you sure $this->colorCli->primary...) - ' . $mov['imdbid']) of type void can be used in concatenation? ( Ignorable by Annotation )

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

465
            /** @scrutinizer ignore-type */ $this->colorCli->primary(
Loading history...
466
                $mov['title'].
467
                ' ('.
468
                $mov['year'].
469
                ') - '.
470
                $mov['imdbid']
471
            );
472
        }
473
474
        return $movieID;
475
    }
476
477
    /**
478
     * Fetch FanArt.tv backdrop / cover / title.
479
     */
480
    protected function fetchFanartTVProperties(string $imdbId): false|array
481
    {
482
        if (! $this->fanart->isConfigured()) {
483
            return false;
484
        }
485
486
        try {
487
            $result = $this->fanart->getMovieProperties($imdbId);
488
489
            if ($result !== null) {
490
                if ($this->echooutput) {
491
                    $this->colorCli->info('Fanart found '.$result['title']);
492
                }
493
494
                return $result;
495
            }
496
        } catch (\Throwable $e) {
497
            Log::warning('FanartTV API error for '.$imdbId.': '.$e->getMessage());
498
        }
499
500
        return false;
501
    }
502
503
    /**
504
     * Fetch movie information from TMDB using an IMDB ID.
505
     */
506
    public function fetchTMDBProperties(string $imdbId, bool $text = false): array|false
507
    {
508
        $lookupId = $text === false && (strlen($imdbId) === 7 || strlen($imdbId) === 8) ? 'tt'.$imdbId : $imdbId;
509
510
        $cacheKey = 'tmdb_movie_'.md5($lookupId);
511
        $expiresAt = now()->addDays(7);
512
513
        if (Cache::has($cacheKey)) {
514
            return Cache::get($cacheKey);
515
        }
516
517
        try {
518
            $tmdbClient = app(TmdbClient::class);
519
520
            if (! $tmdbClient->isConfigured()) {
521
                return false;
522
            }
523
524
            $tmdbLookup = $tmdbClient->getMovie($lookupId, ['credits']);
525
526
            if ($tmdbLookup === null || empty($tmdbLookup)) {
527
                Cache::put($cacheKey, false, $expiresAt);
528
529
                return false;
530
            }
531
532
            $title = TmdbClient::getString($tmdbLookup, 'title');
533
            if ($this->currentTitle !== '' && ! empty($title)) {
534
                similar_text($this->currentTitle, $title, $percent);
535
                if ($percent < self::MATCH_PERCENT) {
536
                    Cache::put($cacheKey, false, $expiresAt);
537
538
                    return false;
539
                }
540
            }
541
542
            $releaseDate = TmdbClient::getString($tmdbLookup, 'release_date');
543
            if ($this->currentYear !== '' && ! empty($releaseDate)) {
544
                $tmdbYear = Carbon::parse($releaseDate)->year;
545
546
                similar_text($this->currentYear, (string) $tmdbYear, $percent);
547
                if ($percent < self::YEAR_MATCH_PERCENT) {
548
                    Cache::put($cacheKey, false, $expiresAt);
549
550
                    return false;
551
                }
552
            }
553
554
            $imdbIdFromResponse = TmdbClient::getString($tmdbLookup, 'imdb_id');
555
            $ret = [
556
                'title' => $title,
557
                'tmdbid' => TmdbClient::getInt($tmdbLookup, 'id'),
558
                'imdbid' => str_replace('tt', '', $imdbIdFromResponse),
559
                'rating' => '',
560
                'actors' => '',
561
                'director' => '',
562
                'plot' => TmdbClient::getString($tmdbLookup, 'overview'),
563
                'tagline' => TmdbClient::getString($tmdbLookup, 'tagline'),
564
                'year' => '',
565
                'genre' => '',
566
                'cover' => '',
567
                'backdrop' => '',
568
            ];
569
570
            $vote = TmdbClient::getFloat($tmdbLookup, 'vote_average');
571
            if ($vote > 0) {
572
                $ret['rating'] = $vote;
573
            }
574
575
            $credits = TmdbClient::getArray($tmdbLookup, 'credits');
576
            $cast = TmdbClient::getArray($credits, 'cast');
577
            if (! empty($cast)) {
578
                $actors = [];
579
                foreach ($cast as $member) {
580
                    if (is_array($member) && ! empty($member['name'])) {
581
                        $actors[] = $member['name'];
582
                    }
583
                }
584
                if (! empty($actors)) {
585
                    $ret['actors'] = $actors;
586
                }
587
            }
588
589
            $crew = TmdbClient::getArray($credits, 'crew');
590
            foreach ($crew as $crewMember) {
591
                if (! is_array($crewMember)) {
592
                    continue;
593
                }
594
                $department = TmdbClient::getString($crewMember, 'department');
595
                $job = TmdbClient::getString($crewMember, 'job');
596
                if ($department === 'Directing' && $job === 'Director') {
597
                    $ret['director'] = TmdbClient::getString($crewMember, 'name');
598
                    break;
599
                }
600
            }
601
602
            if (! empty($releaseDate)) {
603
                $ret['year'] = Carbon::parse($releaseDate)->year;
604
            }
605
606
            $genresa = TmdbClient::getArray($tmdbLookup, 'genres');
607
            if (! empty($genresa)) {
608
                $genres = [];
609
                foreach ($genresa as $genre) {
610
                    if (is_array($genre) && ! empty($genre['name'])) {
611
                        $genres[] = $genre['name'];
612
                    }
613
                }
614
                if (! empty($genres)) {
615
                    $ret['genre'] = $genres;
616
                }
617
            }
618
619
            $posterPath = TmdbClient::getString($tmdbLookup, 'poster_path');
620
            if (! empty($posterPath)) {
621
                $ret['cover'] = 'https://image.tmdb.org/t/p/original'.$posterPath;
622
            }
623
624
            $backdropPath = TmdbClient::getString($tmdbLookup, 'backdrop_path');
625
            if (! empty($backdropPath)) {
626
                $ret['backdrop'] = 'https://image.tmdb.org/t/p/original'.$backdropPath;
627
            }
628
629
            if ($this->echooutput) {
630
                $this->colorCli->info('TMDb found '.$ret['title']);
631
            }
632
633
            Cache::put($cacheKey, $ret, $expiresAt);
634
635
            return $ret;
636
637
        } catch (\Throwable $e) {
638
            Log::warning('TMDB API error for '.$lookupId.': '.$e->getMessage());
639
            Cache::put($cacheKey, false, now()->addHours(6));
640
641
            return false;
642
        }
643
    }
644
645
    /**
646
     * Fetch movie information from IMDB.
647
     */
648
    public function fetchIMDBProperties(string $imdbId): array|false
649
    {
650
        $cacheKey = 'imdb_movie_'.md5($imdbId);
651
        $expiresAt = now()->addDays(7);
652
        if (Cache::has($cacheKey)) {
653
            return Cache::get($cacheKey);
654
        }
655
        try {
656
            $scraper = app(ImdbScraper::class);
657
            $scraped = $scraper->fetchById($imdbId);
658
            if ($scraped === false || empty($scraped['title'])) {
659
                Cache::put($cacheKey, false, now()->addHours(6));
660
661
                return false;
662
            }
663
            if (! empty($this->currentTitle)) {
664
                similar_text($this->currentTitle, $scraped['title'], $percent);
665
                if ($percent < self::MATCH_PERCENT) {
666
                    Cache::put($cacheKey, false, now()->addHours(6));
667
668
                    return false;
669
                }
670
                if (! empty($this->currentYear) && ! empty($scraped['year'])) {
671
                    similar_text($this->currentYear, $scraped['year'], $yearPercent);
672
                    if ($yearPercent < self::YEAR_MATCH_PERCENT) {
673
                        Cache::put($cacheKey, false, now()->addHours(6));
674
675
                        return false;
676
                    }
677
                }
678
            }
679
            Cache::put($cacheKey, $scraped, $expiresAt);
680
            if ($this->echooutput) {
681
                $this->colorCli->info('IMDb scraped '.$scraped['title']);
682
            }
683
684
            return $scraped;
685
        } catch (\Throwable $e) {
686
            Log::warning('IMDb scrape error for '.$imdbId.': '.$e->getMessage());
687
            Cache::put($cacheKey, false, now()->addHours(6));
688
689
            return false;
690
        }
691
    }
692
693
    /**
694
     * Fetch movie information from Trakt.tv using IMDB ID.
695
     *
696
     * @throws GuzzleException
697
     */
698
    public function fetchTraktTVProperties(string $imdbId): array|false
699
    {
700
        if ($this->traktcheck === null) {
701
            return false;
702
        }
703
704
        $cacheKey = 'trakt_movie_'.md5($imdbId);
705
        $expiresAt = now()->addDays(7);
706
707
        if (Cache::has($cacheKey)) {
708
            return Cache::get($cacheKey);
709
        }
710
711
        try {
712
            $resp = $this->traktTv->client->getMovieSummary('tt'.$imdbId, 'full');
713
714
            if ($resp === false || empty($resp['title'])) {
715
                Cache::put($cacheKey, false, now()->addHours(6));
716
717
                return false;
718
            }
719
720
            if (! empty($this->currentTitle)) {
721
                similar_text($this->currentTitle, $resp['title'], $percent);
722
                if ($percent < self::MATCH_PERCENT) {
723
                    Cache::put($cacheKey, false, now()->addHours(6));
724
725
                    return false;
726
                }
727
            }
728
729
            if (! empty($this->currentYear) && ! empty($resp['year'])) {
730
                similar_text($this->currentYear, $resp['year'], $percent);
731
                if ($percent < self::YEAR_MATCH_PERCENT) {
732
                    Cache::put($cacheKey, false, now()->addHours(6));
733
734
                    return false;
735
                }
736
            }
737
738
            $movieData = [
739
                'id' => $resp['ids']['trakt'] ?? null,
740
                'title' => $resp['title'],
741
                'overview' => $resp['overview'] ?? '',
742
                'tagline' => $resp['tagline'] ?? '',
743
                'year' => $resp['year'] ?? '',
744
                'genres' => $resp['genres'] ?? '',
745
                'rating' => $resp['rating'] ?? '',
746
                'votes' => $resp['votes'] ?? 0,
747
                'language' => $resp['language'] ?? '',
748
                'runtime' => $resp['runtime'] ?? 0,
749
                'trailer' => $resp['trailer'] ?? '',
750
            ];
751
752
            if ($this->echooutput) {
753
                $this->colorCli->info('Trakt found '.$movieData['title']);
754
            }
755
756
            Cache::put($cacheKey, $movieData, $expiresAt);
757
758
            return $movieData;
759
760
        } catch (\Throwable $e) {
761
            Log::warning('Trakt API error for '.$imdbId.': '.$e->getMessage());
762
            Cache::put($cacheKey, false, now()->addHours(6));
763
764
            return false;
765
        }
766
    }
767
768
    /**
769
     * Fetch movie information from OMDB API using IMDB ID.
770
     */
771
    public function fetchOmdbAPIProperties(string $imdbId): array|false
772
    {
773
        if ($this->omdbapikey === null) {
774
            return false;
775
        }
776
777
        $cacheKey = 'omdb_movie_'.md5($imdbId);
778
        $expiresAt = now()->addDays(7);
779
780
        if (Cache::has($cacheKey)) {
781
            return Cache::get($cacheKey);
782
        }
783
784
        try {
785
            $resp = $this->omdbApi->fetch('i', 'tt'.$imdbId);
0 ignored issues
show
Bug introduced by
The method fetch() does not exist on null. ( Ignorable by Annotation )

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

785
            /** @scrutinizer ignore-call */ 
786
            $resp = $this->omdbApi->fetch('i', 'tt'.$imdbId);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
786
787
            if (! is_object($resp) ||
0 ignored issues
show
introduced by
The condition is_object($resp) is always false.
Loading history...
788
                $resp->message !== 'OK' ||
789
                Str::contains($resp->data->Response, 'Error:') ||
790
                $resp->data->Response === 'False') {
791
792
                Cache::put($cacheKey, false, now()->addHours(6));
793
794
                return false;
795
            }
796
797
            if (! empty($this->currentTitle)) {
798
                similar_text($this->currentTitle, $resp->data->Title, $percent);
799
                if ($percent < self::MATCH_PERCENT) {
800
                    Cache::put($cacheKey, false, now()->addHours(6));
801
802
                    return false;
803
                }
804
805
                if (! empty($this->currentYear)) {
806
                    similar_text($this->currentYear, $resp->data->Year, $percent);
807
                    if ($percent < self::YEAR_MATCH_PERCENT) {
808
                        Cache::put($cacheKey, false, now()->addHours(6));
809
810
                        return false;
811
                    }
812
                }
813
            }
814
815
            $rtRating = '';
816
            if (isset($resp->data->Ratings) && is_array($resp->data->Ratings) && count($resp->data->Ratings) > 1) {
817
                $rtRating = $resp->data->Ratings[1]->Value ?? '';
818
            }
819
820
            $movieData = [
821
                'title' => $resp->data->Title ?? '',
822
                'cover' => $resp->data->Poster ?? '',
823
                'genre' => $resp->data->Genre ?? '',
824
                'year' => $resp->data->Year ?? '',
825
                'plot' => $resp->data->Plot ?? '',
826
                'rating' => $resp->data->imdbRating ?? '',
827
                'rtRating' => $rtRating,
828
                'tagline' => $resp->data->Tagline ?? '',
829
                'director' => $resp->data->Director ?? '',
830
                'actors' => $resp->data->Actors ?? '',
831
                'language' => $resp->data->Language ?? '',
832
                'boxOffice' => $resp->data->BoxOffice ?? '',
833
            ];
834
835
            if ($this->echooutput) {
836
                $this->colorCli->info('OMDbAPI Found '.$movieData['title']);
837
            }
838
839
            Cache::put($cacheKey, $movieData, $expiresAt);
840
841
            return $movieData;
842
843
        } catch (\Throwable $e) {
844
            Log::warning('OMDB API error for '.$imdbId.': '.$e->getMessage());
845
            Cache::put($cacheKey, false, now()->addHours(6));
846
847
            return false;
848
        }
849
    }
850
851
    /**
852
     * Update a release with an IMDB ID and related movie information.
853
     *
854
     * @throws \Exception
855
     */
856
    public function doMovieUpdate(string $buffer, string $service, int $id, int $processImdb = 1): string|false
857
    {
858
        $existingImdbId = Release::query()->where('id', $id)->value('imdbid');
859
        if ($existingImdbId !== null && $existingImdbId !== '' && $existingImdbId !== '0000000') {
860
            return $existingImdbId;
861
        }
862
863
        $imdbId = false;
864
        if (preg_match('/(?:imdb.*?)?(?:tt|Title\?)(?P<imdbid>\d{5,8})/i', $buffer, $hits)) {
865
            $imdbId = $hits['imdbid'];
866
        }
867
868
        if ($imdbId !== false) {
869
            try {
870
                $this->service = $service;
871
                if ($this->echooutput && $this->service !== '') {
872
                    $this->colorCli->info($this->service.' found IMDBid: tt'.$imdbId);
873
                }
874
875
                $movieInfoId = MovieInfo::query()->where('imdbid', $imdbId)->first(['id']);
876
877
                Release::query()->where('id', $id)->update([
878
                    'imdbid' => $imdbId,
879
                    'movieinfo_id' => $movieInfoId !== null ? $movieInfoId['id'] : null,
880
                ]);
881
882
                if ($processImdb === 1) {
883
                    $movCheck = $this->getMovieInfo($imdbId);
884
                    $thirtyDaysInSeconds = 30 * 24 * 60 * 60;
885
886
                    if ($movCheck === null ||
887
                        (isset($movCheck['updated_at']) &&
888
                            (time() - strtotime($movCheck['updated_at'])) > $thirtyDaysInSeconds)) {
889
890
                        $info = $this->updateMovieInfo($imdbId);
891
892
                        if ($info === false) {
893
                            Release::query()->where('id', $id)->update(['imdbid' => '0000000']);
894
                        } elseif ($info === true) {
0 ignored issues
show
introduced by
The condition $info === true is always true.
Loading history...
895
                            $freshMovieInfo = MovieInfo::query()->where('imdbid', $imdbId)->first(['id']);
896
897
                            Release::query()->where('id', $id)->update([
898
                                'movieinfo_id' => $freshMovieInfo !== null ? $freshMovieInfo['id'] : null,
899
                            ]);
900
                        }
901
                    }
902
                }
903
904
                return $imdbId;
905
            } catch (\Exception $e) {
906
                Log::error('Error updating movie information: '.$e->getMessage());
907
908
                return false;
909
            }
910
        }
911
912
        return $imdbId;
913
    }
914
915
    /**
916
     * Process releases with no IMDB IDs by looking up movie information from various sources.
917
     *
918
     * @throws \Exception
919
     * @throws GuzzleException
920
     */
921
    public function processMovieReleases(string $groupID = '', string $guidChar = '', int $lookupIMDB = 1): void
922
    {
923
        if ($lookupIMDB === 0) {
924
            return;
925
        }
926
927
        $query = Release::query()
928
            ->select(['searchname', 'id'])
929
            ->whereBetween('categories_id', [Category::MOVIE_ROOT, Category::MOVIE_OTHER])
930
            ->whereNull('imdbid');
931
932
        if ($groupID !== '') {
933
            $query->where('groups_id', $groupID);
934
        }
935
936
        if ($guidChar !== '') {
937
            $query->where('leftguid', $guidChar);
938
        }
939
940
        if ((int) $lookupIMDB === 2) {
941
            $query->where('isrenamed', '=', 1);
942
        }
943
944
        $res = $query->orderByDesc('id')->limit($this->movieqty)->get();
0 ignored issues
show
Bug introduced by
'id' of type string is incompatible with the type Closure|Illuminate\Datab...\Database\Query\Builder expected by parameter $column of Illuminate\Database\Query\Builder::orderByDesc(). ( Ignorable by Annotation )

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

944
        $res = $query->orderByDesc(/** @scrutinizer ignore-type */ 'id')->limit($this->movieqty)->get();
Loading history...
945
946
        $movieCount = count($res);
947
        $failedIDs = [];
948
949
        if ($movieCount > 0) {
950
            if ($this->echooutput && $movieCount > 1) {
951
                $this->colorCli->header('Processing '.$movieCount.' movie releases.');
952
            }
953
954
            foreach ($res as $arr) {
955
                if (! $this->parseMovieSearchName($arr['searchname'])) {
956
                    $failedIDs[] = $arr['id'];
957
958
                    continue;
959
                }
960
961
                $this->currentRelID = $arr['id'];
962
                $movieName = $this->formatMovieName();
963
964
                if ($this->echooutput) {
965
                    $this->colorCli->info('Looking up: '.$movieName);
966
                }
967
968
                $foundIMDB = $this->searchLocalDatabase($arr['id']) ||
969
                    $this->searchIMDb($arr['id']) ||
970
                    $this->searchOMDbAPI($arr['id']) ||
971
                    $this->searchTraktTV($arr['id'], $movieName) ||
972
                    $this->searchTMDB($arr['id']);
973
974
                if ($foundIMDB) {
975
                    if ($this->echooutput) {
976
                        $this->colorCli->primary('Successfully updated release with IMDB ID');
977
                    }
978
979
                    continue;
980
                } else {
981
                    $releaseCheck = Release::query()->where('id', $arr['id'])->whereNotNull('imdbid')->exists();
982
                    if ($releaseCheck) {
983
                        if ($this->echooutput) {
984
                            $this->colorCli->info('Release already has IMDB ID, skipping');
985
                        }
986
987
                        continue;
988
                    }
989
                }
990
991
                $failedIDs[] = $arr['id'];
992
            }
993
994
            if (! empty($failedIDs)) {
995
                if ($this->echooutput) {
996
                    $failedReleases = Release::query()
997
                        ->select(['id', 'searchname'])
998
                        ->whereIn('id', $failedIDs)
999
                        ->get();
1000
1001
                    $this->colorCli->header('Failed to find IMDB IDs for '.count($failedIDs).' releases:');
1002
                    foreach ($failedReleases as $release) {
1003
                        $this->colorCli->error("ID: {$release->id} - {$release->searchname}");
1004
                    }
1005
                }
1006
1007
                foreach (array_chunk($failedIDs, 100) as $chunk) {
1008
                    Release::query()->whereIn('id', $chunk)->update(['imdbid' => '0000000']);
1009
                }
1010
            }
1011
        }
1012
    }
1013
1014
    private function formatMovieName(): string
1015
    {
1016
        $movieName = $this->currentTitle;
1017
        if ($this->currentYear !== '') {
1018
            $movieName .= ' ('.$this->currentYear.')';
1019
        }
1020
1021
        return $movieName;
1022
    }
1023
1024
    private function searchLocalDatabase(int $releaseId): bool
1025
    {
1026
        $getIMDBid = $this->localIMDBSearch();
1027
        if ($getIMDBid === false) {
0 ignored issues
show
introduced by
The condition $getIMDBid === false is always true.
Loading history...
1028
            return false;
1029
        }
1030
1031
        $imdbId = $this->doMovieUpdate('tt'.$getIMDBid, 'Local DB', $releaseId);
1032
1033
        return $imdbId !== false;
1034
    }
1035
1036
    private function searchIMDb(int $releaseId): bool
1037
    {
1038
        try {
1039
            $scraper = app(ImdbScraper::class);
1040
            $matches = $scraper->search($this->currentTitle);
1041
            foreach ($matches as $match) {
1042
                $title = $match['title'] ?? '';
1043
                if ($title === '') {
1044
                    continue;
1045
                }
1046
                similar_text($title, $this->currentTitle, $percent);
1047
                if ($percent < self::MATCH_PERCENT) {
1048
                    continue;
1049
                }
1050
                if (! empty($this->currentYear) && ! empty($match['year'])) {
1051
                    similar_text($this->currentYear, $match['year'], $yearPercent);
1052
                    if ($yearPercent < self::YEAR_MATCH_PERCENT) {
1053
                        continue;
1054
                    }
1055
                }
1056
                $imdbId = $this->doMovieUpdate('tt'.$match['imdbid'], 'IMDb(scrape)', $releaseId);
1057
                if ($imdbId !== false) {
1058
                    return true;
1059
                }
1060
            }
1061
        } catch (\Throwable $e) {
1062
            Log::debug('IMDb scraper search failed: '.$e->getMessage());
1063
        }
1064
1065
        return false;
1066
    }
1067
1068
    private function searchOMDbAPI(int $releaseId): bool
1069
    {
1070
        if ($this->omdbapikey === null) {
1071
            return false;
1072
        }
1073
1074
        $omdbTitle = strtolower(str_replace(' ', '_', $this->currentTitle));
1075
1076
        try {
1077
            $buffer = $this->currentYear !== ''
1078
                ? $this->omdbApi->search($omdbTitle, 'movie', $this->currentYear)
1079
                : $this->omdbApi->search($omdbTitle, 'movie');
1080
1081
            if (! is_object($buffer) ||
0 ignored issues
show
introduced by
The condition is_object($buffer) is always false.
Loading history...
1082
                $buffer->message !== 'OK' ||
1083
                Str::contains($buffer->data->Response, 'Error:') ||
1084
                $buffer->data->Response !== 'True' ||
1085
                empty($buffer->data->Search[0]->imdbID)) {
1086
                return false;
1087
            }
1088
1089
            $getIMDBid = $buffer->data->Search[0]->imdbID;
1090
            $imdbId = $this->doMovieUpdate($getIMDBid, 'OMDbAPI', $releaseId);
1091
1092
            return $imdbId !== false;
1093
1094
        } catch (\Exception $e) {
1095
            Log::error('OMDb API error: '.$e->getMessage());
1096
1097
            return false;
1098
        }
1099
    }
1100
1101
    private function searchTraktTV(int $releaseId, string $movieName): bool
1102
    {
1103
        if ($this->traktcheck === null) {
1104
            return false;
1105
        }
1106
1107
        try {
1108
            $data = $this->traktTv->client->getMovieSummary($movieName, 'full');
1109
            if ($data === false || empty($data['ids']['imdb'])) {
1110
                return false;
1111
            }
1112
1113
            $this->parseTraktTv($data);
1114
            $imdbId = $this->doMovieUpdate($data['ids']['imdb'], 'Trakt', $releaseId);
1115
1116
            return $imdbId !== false;
1117
1118
        } catch (\Exception $e) {
1119
            Log::error('Trakt.tv error: '.$e->getMessage());
1120
1121
            return false;
1122
        }
1123
    }
1124
1125
    private function searchTMDB(int $releaseId): bool
1126
    {
1127
        try {
1128
            $tmdbClient = app(TmdbClient::class);
1129
1130
            if (! $tmdbClient->isConfigured()) {
1131
                return false;
1132
            }
1133
1134
            $data = $tmdbClient->searchMovies($this->currentTitle);
1135
1136
            if ($data === null || empty($data['total_results']) || empty($data['results'])) {
1137
                return false;
1138
            }
1139
1140
            $results = TmdbClient::getArray($data, 'results');
1141
            foreach ($results as $result) {
1142
                if (! is_array($result)) {
1143
                    continue;
1144
                }
1145
1146
                $resultId = TmdbClient::getInt($result, 'id');
1147
                $releaseDate = TmdbClient::getString($result, 'release_date');
1148
1149
                if ($resultId === 0 || empty($releaseDate)) {
1150
                    continue;
1151
                }
1152
1153
                similar_text(
1154
                    $this->currentYear,
1155
                    (string) Carbon::parse($releaseDate)->year,
1156
                    $percent
1157
                );
1158
1159
                if ($percent < self::YEAR_MATCH_PERCENT) {
1160
                    continue;
1161
                }
1162
1163
                $ret = $this->fetchTMDBProperties((string) $resultId, true);
1164
                if ($ret === false || empty($ret['imdbid'])) {
1165
                    continue;
1166
                }
1167
1168
                $imdbId = $this->doMovieUpdate('tt'.$ret['imdbid'], 'TMDB', $releaseId);
1169
                if ($imdbId !== false) {
1170
                    return true;
1171
                }
1172
            }
1173
1174
        } catch (\Throwable $e) {
1175
            Log::warning('TMDB API error: '.$e->getMessage());
1176
        }
1177
1178
        return false;
1179
    }
1180
1181
    protected function localIMDBSearch(): string|false
1182
    {
1183
        if (empty($this->currentTitle)) {
1184
            return false;
1185
        }
1186
1187
        $cacheKey = 'local_imdb_'.md5($this->currentTitle.$this->currentYear);
1188
1189
        if (Cache::has($cacheKey)) {
1190
            return Cache::get($cacheKey);
1191
        }
1192
1193
        $query = MovieInfo::query()
1194
            ->select(['imdbid', 'title'])
1195
            ->where('title', 'like', '%'.$this->currentTitle.'%');
1196
1197
        if (! empty($this->currentYear)) {
1198
            $start = Carbon::createFromFormat('Y', $this->currentYear)->subYears(2)->year;
0 ignored issues
show
Bug introduced by
The method subYears() does not exist on DateTime. It seems like you code against a sub-type of DateTime such as Carbon\Carbon. ( Ignorable by Annotation )

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

1198
            $start = Carbon::createFromFormat('Y', $this->currentYear)->/** @scrutinizer ignore-call */ subYears(2)->year;
Loading history...
1199
            $end = Carbon::createFromFormat('Y', $this->currentYear)->addYears(2)->year;
0 ignored issues
show
Bug introduced by
The method addYears() does not exist on DateTime. It seems like you code against a sub-type of DateTime such as Carbon\Carbon. ( Ignorable by Annotation )

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

1199
            $end = Carbon::createFromFormat('Y', $this->currentYear)->/** @scrutinizer ignore-call */ addYears(2)->year;
Loading history...
1200
            $query->whereBetween('year', [$start, $end]);
1201
        }
1202
1203
        $potentialMatches = $query->get();
1204
1205
        if ($potentialMatches->isEmpty()) {
1206
            Cache::put($cacheKey, false, now()->addHours(6));
1207
1208
            return false;
1209
        }
1210
1211
        foreach ($potentialMatches as $match) {
1212
            similar_text($this->currentTitle, $match['title'], $percent);
1213
1214
            if ($percent >= self::MATCH_PERCENT) {
1215
                Cache::put($cacheKey, $match['imdbid'], now()->addDays(7));
1216
1217
                if ($this->echooutput) {
1218
                    $this->colorCli->info("Found local match: {$match['title']} ({$match['imdbid']})");
1219
                }
1220
1221
                return $match['imdbid'];
1222
            }
1223
        }
1224
1225
        Cache::put($cacheKey, false, now()->addHours(6));
1226
1227
        return false;
1228
    }
1229
1230
    protected function parseMovieSearchName(string $releaseName): bool
1231
    {
1232
        if (empty(trim($releaseName))) {
1233
            return false;
1234
        }
1235
1236
        $cacheKey = 'parse_movie_'.md5($releaseName);
1237
1238
        if (Cache::has($cacheKey)) {
1239
            $result = Cache::get($cacheKey);
1240
            if (is_array($result)) {
1241
                $this->currentTitle = $result['title'];
1242
                $this->currentYear = $result['year'];
1243
1244
                return true;
1245
            }
1246
1247
            return false;
1248
        }
1249
1250
        $name = $year = '';
1251
1252
        $followingList = '[^\w]((1080|480|720|2160)p|AC3D|Directors([^\w]CUT)?|DD5\.1|(DVD|BD|BR|UHD)(Rip)?|'
1253
            .'BluRay|divx|HDTV|iNTERNAL|LiMiTED|(Real\.)?PROPER|RE(pack|Rip)|Sub\.?(fix|pack)|'
1254
            .'Unrated|WEB-?DL|WEBRip|(x|H|HEVC)[ ._-]?26[45]|xvid|AAC|REMUX)[^\w]';
1255
1256
        if (preg_match('/(?P<name>[\w. -]+)[^\w](?P<year>(19|20)\d\d)/i', $releaseName, $hits)) {
1257
            $name = $hits['name'];
1258
            $year = $hits['year'];
1259
        } elseif (preg_match('/([^\w]{2,})?(?P<name>[\w .-]+?)'.$followingList.'/i', $releaseName, $hits)) {
1260
            $name = $hits['name'];
1261
        } elseif (preg_match('/^(?P<name>[\w .-]+?)'.$followingList.'/i', $releaseName, $hits)) {
1262
            $name = $hits['name'];
1263
        } elseif (strlen($releaseName) <= 100 && ! preg_match('/\.(rar|zip|avi|mkv|mp4)$/i', $releaseName)) {
1264
            $name = $releaseName;
1265
        }
1266
1267
        if (! empty($name)) {
1268
            $name = preg_replace('/'.$followingList.'/i', ' ', $name);
1269
            $name = preg_replace('/\([^)]*\)/i', ' ', $name);
1270
            while (($openPos = strpos($name, '[')) !== false && ($closePos = strpos($name, ']', $openPos)) !== false) {
1271
                $name = substr($name, 0, $openPos).' '.substr($name, $closePos + 1);
1272
            }
1273
            $name = str_replace(['.', '_'], ' ', $name);
1274
            $name = preg_replace('/-[A-Z0-9].*$/i', '', $name);
1275
            $name = trim(preg_replace('/\s{2,}/', ' ', $name));
1276
1277
            if (strlen($name) > 2 && ! preg_match('/^\d+$/', $name)) {
1278
                $this->currentTitle = $name;
1279
                $this->currentYear = $year;
1280
1281
                Cache::put($cacheKey, [
1282
                    'title' => $name,
1283
                    'year' => $year,
1284
                ], now()->addDays(7));
1285
1286
                return true;
1287
            }
1288
        }
1289
1290
        Cache::put($cacheKey, false, now()->addHours(24));
1291
1292
        return false;
1293
    }
1294
1295
    /**
1296
     * Get IMDB genres.
1297
     */
1298
    public function getGenres(): array
1299
    {
1300
        return [
1301
            'Action',
1302
            'Adventure',
1303
            'Animation',
1304
            'Biography',
1305
            'Comedy',
1306
            'Crime',
1307
            'Documentary',
1308
            'Drama',
1309
            'Family',
1310
            'Fantasy',
1311
            'Film-Noir',
1312
            'Game-Show',
1313
            'History',
1314
            'Horror',
1315
            'Music',
1316
            'Musical',
1317
            'Mystery',
1318
            'News',
1319
            'Reality-TV',
1320
            'Romance',
1321
            'Sci-Fi',
1322
            'Sport',
1323
            'Talk-Show',
1324
            'Thriller',
1325
            'War',
1326
            'Western',
1327
        ];
1328
    }
1329
1330
    protected function hasCover(string $imdbId): bool
1331
    {
1332
        $record = MovieInfo::query()->select('cover')->where('imdbid', $imdbId)->first();
1333
        $dbHas = $record !== null && (int) $record->cover === 1;
1334
        $filePath = $this->imgSavePath.$imdbId.'-cover.jpg';
1335
        $fileHas = File::isFile($filePath);
1336
1337
        return $dbHas || $fileHas;
1338
    }
1339
1340
    protected function fetchAndSaveCoverOnly(string $imdbId): bool
1341
    {
1342
        try {
1343
            $fanart = $this->fetchFanartTVProperties($imdbId);
1344
            if (! empty($fanart['cover'])) {
1345
                if ($this->releaseImage->saveImage($imdbId.'-cover', $fanart['cover'], $this->imgSavePath)) {
1346
                    return true;
1347
                }
1348
            }
1349
        } catch (\Throwable $e) {
1350
            Log::debug('Fanart cover fetch failed for '.$imdbId.': '.$e->getMessage());
1351
        }
1352
1353
        try {
1354
            $tmdb = $this->fetchTMDBProperties($imdbId);
1355
            if (! empty($tmdb['cover'])) {
1356
                if ($this->releaseImage->saveImage($imdbId.'-cover', $tmdb['cover'], $this->imgSavePath)) {
1357
                    return true;
1358
                }
1359
            }
1360
        } catch (\Throwable $e) {
1361
            Log::debug('TMDB cover fetch failed for '.$imdbId.': '.$e->getMessage());
1362
        }
1363
1364
        try {
1365
            $imdb = $this->fetchIMDBProperties($imdbId);
1366
            if (! empty($imdb['cover'])) {
1367
                if ($this->releaseImage->saveImage($imdbId.'-cover', $imdb['cover'], $this->imgSavePath)) {
1368
                    return true;
1369
                }
1370
            }
1371
        } catch (\Throwable $e) {
1372
            Log::debug('IMDB cover fetch failed for '.$imdbId.': '.$e->getMessage());
1373
        }
1374
1375
        try {
1376
            $omdb = $this->fetchOmdbAPIProperties($imdbId);
1377
            if (! empty($omdb['cover'])) {
1378
                if ($this->releaseImage->saveImage($imdbId.'-cover', $omdb['cover'], $this->imgSavePath)) {
1379
                    return true;
1380
                }
1381
            }
1382
        } catch (\Throwable $e) {
1383
            Log::debug('OMDB cover fetch failed for '.$imdbId.': '.$e->getMessage());
1384
        }
1385
1386
        return false;
1387
    }
1388
}
1389
1390