MovieService::localIMDBSearch()   B
last analyzed

Complexity

Conditions 8
Paths 12

Size

Total Lines 47
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

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

467
            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...
468
            $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

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

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

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

1201
            $start = Carbon::createFromFormat('Y', $this->currentYear)->/** @scrutinizer ignore-call */ subYears(2)->year;
Loading history...
1202
            $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

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