MovieService::processMovieReleases()   F
last analyzed

Complexity

Conditions 23
Paths 4169

Size

Total Lines 88
Code Lines 53

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 53
c 1
b 0
f 0
dl 0
loc 88
rs 0
cc 23
nc 4169
nop 3

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
        if ($imdbId === null || $imdbId === '' || $imdbId === '0000000') {
108
            return null;
109
        }
110
111
        return MovieInfo::query()->where('imdbid', $imdbId)->first();
112
    }
113
114
    /**
115
     * Get trailer using IMDB Id.
116
     *
117
     * @throws \Exception
118
     * @throws GuzzleException
119
     */
120
    public function getTrailer(int $imdbId): string|false
121
    {
122
        $trailer = MovieInfo::query()->where('imdbid', $imdbId)->where('trailer', '<>', '')->first(['trailer']);
123
        if ($trailer !== null) {
124
            return $trailer['trailer'];
125
        }
126
127
        if ($this->traktcheck !== null) {
128
            $data = $this->traktTv->client->getMovieSummary('tt'.$imdbId, 'full');
129
            if (($data !== false) && ! empty($data['trailer'])) {
130
                return $data['trailer'];
131
            }
132
        }
133
134
        $trailer = imdb_trailers($imdbId);
135
        if ($trailer) {
136
            MovieInfo::query()->where('imdbid', $imdbId)->update(['trailer' => $trailer]);
137
138
            return $trailer;
139
        }
140
141
        return false;
142
    }
143
144
    /**
145
     * Parse trakt info, insert into DB.
146
     */
147
    public function parseTraktTv(array &$data): mixed
148
    {
149
        if (empty($data['ids']['imdb'])) {
150
            return false;
151
        }
152
153
        if (! empty($data['trailer'])) {
154
            $data['trailer'] = str_ireplace(
155
                ['watch?v=', 'http://'],
156
                ['embed/', 'https://'],
157
                $data['trailer']
158
            );
159
        }
160
        $imdbId = (str_starts_with($data['ids']['imdb'], 'tt')) ? substr($data['ids']['imdb'], 2) : $data['ids']['imdb'];
161
        $cover = 0;
162
        if (File::isFile($this->imgSavePath.$imdbId.'-cover.jpg')) {
163
            $cover = 1;
164
        }
165
166
        return $this->update([
167
            'genre' => implode(', ', $data['genres']),
168
            'imdbid' => $this->checkTraktValue($imdbId),
169
            'language' => $this->checkTraktValue($data['language']),
170
            'plot' => $this->checkTraktValue($data['overview']),
171
            'rating' => $this->checkTraktValue($data['rating']),
172
            'tagline' => $this->checkTraktValue($data['tagline']),
173
            'title' => $this->checkTraktValue($data['title']),
174
            'tmdbid' => $this->checkTraktValue($data['ids']['tmdb']),
175
            'traktid' => $this->checkTraktValue($data['ids']['trakt']),
176
            'trailer' => $this->checkTraktValue($data['trailer']),
177
            'cover' => $cover,
178
            'year' => $this->checkTraktValue($data['year']),
179
        ]);
180
    }
181
182
    private function checkTraktValue(mixed $value): mixed
183
    {
184
        if (\is_array($value) && ! empty($value)) {
185
            $temp = '';
186
            foreach ($value as $val) {
187
                if (! is_array($val) && ! is_object($val)) {
188
                    $temp .= $val;
189
                }
190
            }
191
            $value = $temp;
192
        }
193
194
        return ! empty($value) ? $value : '';
195
    }
196
197
    /**
198
     * Get array of column keys, for inserting / updating.
199
     */
200
    public function getColumnKeys(): array
201
    {
202
        return [
203
            'actors', 'backdrop', 'cover', 'director', 'genre', 'imdbid', 'language',
204
            'plot', 'rating', 'rtrating', 'tagline', 'title', 'tmdbid', 'traktid', 'trailer', 'type', 'year',
205
        ];
206
    }
207
208
    /**
209
     * Choose the first non-empty variable from up to five inputs.
210
     */
211
    protected function setVariables(string|array $variable1, string|array $variable2, string|array $variable3, string|array $variable4, string|array $variable5 = ''): array|string
212
    {
213
        if (! empty($variable1)) {
214
            return $variable1;
215
        }
216
        if (! empty($variable2)) {
217
            return $variable2;
218
        }
219
        if (! empty($variable3)) {
220
            return $variable3;
221
        }
222
        if (! empty($variable4)) {
223
            return $variable4;
224
        }
225
        if (! empty($variable5)) {
226
            return $variable5;
227
        }
228
229
        return '';
230
    }
231
232
    /**
233
     * Update movie on movie-edit page.
234
     */
235
    public function update(array $values): bool
236
    {
237
        if (! count($values)) {
238
            return false;
239
        }
240
241
        $query = [];
242
        $onDuplicateKey = ['created_at' => now()];
243
        $found = 0;
244
        foreach ($values as $key => $value) {
245
            if (! empty($value)) {
246
                $found++;
247
                if (\in_array($key, ['genre', 'language'], false)) {
248
                    $value = substr($value, 0, 64);
249
                }
250
                $query += [$key => $value];
251
                $onDuplicateKey += [$key => $value];
252
            }
253
        }
254
        if (! $found) {
255
            return false;
256
        }
257
        foreach ($query as $key => $value) {
258
            $query[$key] = rtrim($value, ', ');
259
        }
260
261
        MovieInfo::upsert($query, ['imdbid'], $onDuplicateKey);
262
263
        // Always attempt to fetch a missing cover if imdbid present and cover not provided.
264
        if (! empty($query['imdbid'])) {
265
            $imdbIdForCover = $query['imdbid'];
266
            $coverProvided = array_key_exists('cover', $values) && ! empty($values['cover']);
267
            if (! $coverProvided && ! $this->hasCover($imdbIdForCover)) {
268
                if ($this->fetchAndSaveCoverOnly($imdbIdForCover)) {
269
                    MovieInfo::query()->where('imdbid', $imdbIdForCover)->update(['cover' => 1]);
270
                }
271
            }
272
        }
273
274
        return true;
275
    }
276
277
    /**
278
     * Fetch IMDB/TMDB/TRAKT/OMDB/iTunes info for the movie.
279
     *
280
     * @throws \Exception
281
     */
282
    public function updateMovieInfo(string $imdbId): bool
283
    {
284
        if ($this->echooutput && $this->service !== '') {
285
            $this->colorCli->primary('Fetching IMDB info from TMDB/IMDB/Trakt/OMDB/iTunes using IMDB id: '.$imdbId);
286
        }
287
288
        // Check TMDB for IMDB info.
289
        $tmdb = $this->fetchTMDBProperties($imdbId);
290
291
        // Check IMDB for movie info.
292
        $imdb = $this->fetchIMDBProperties($imdbId);
293
294
        // Check TRAKT for movie info
295
        $trakt = $this->fetchTraktTVProperties($imdbId);
296
297
        // Check OMDb for movie info
298
        $omdb = $this->fetchOmdbAPIProperties($imdbId);
299
300
        if (! $imdb && ! $tmdb && ! $trakt && ! $omdb) {
0 ignored issues
show
introduced by
The condition $imdb is always false.
Loading history...
301
            return false;
302
        }
303
304
        // Check FanArt.tv for cover and background images.
305
        $fanart = $this->fetchFanartTVProperties($imdbId);
306
307
        $mov = [];
308
309
        $mov['cover'] = $mov['backdrop'] = $mov['banner'] = 0;
310
        $mov['type'] = $mov['director'] = $mov['actors'] = $mov['language'] = '';
311
312
        $mov['imdbid'] = $imdbId;
313
        $mov['tmdbid'] = (! isset($tmdb['tmdbid']) || $tmdb['tmdbid'] === '') ? 0 : $tmdb['tmdbid'];
314
        $mov['traktid'] = (! isset($trakt['id']) || $trakt['id'] === '') ? 0 : $trakt['id'];
315
316
        // Prefer Fanart.tv cover over TMDB,TMDB over IMDB,IMDB over OMDB and OMDB over iTunes.
317
        if (! empty($fanart['cover'])) {
318
            try {
319
                $mov['cover'] = $this->releaseImage->saveImage($imdbId.'-cover', $fanart['cover'], $this->imgSavePath);
320
                if ($mov['cover'] === 0) {
321
                    Log::warning('Failed to save FanartTV cover for '.$imdbId.' from URL: '.$fanart['cover']);
322
                }
323
            } catch (\Throwable $e) {
324
                Log::error('Error saving FanartTV cover for '.$imdbId.': '.$e->getMessage());
325
                $mov['cover'] = 0;
326
            }
327
        }
328
329
        if ($mov['cover'] === 0 && ! empty($tmdb['cover'])) {
330
            try {
331
                $mov['cover'] = $this->releaseImage->saveImage($imdbId.'-cover', $tmdb['cover'], $this->imgSavePath);
332
                if ($mov['cover'] === 0) {
333
                    Log::warning('Failed to save TMDB cover for '.$imdbId.' from URL: '.$tmdb['cover']);
334
                }
335
            } catch (\Throwable $e) {
336
                Log::error('Error saving TMDB cover for '.$imdbId.': '.$e->getMessage());
337
                $mov['cover'] = 0;
338
            }
339
        }
340
341
        if ($mov['cover'] === 0 && ! empty($imdb['cover'])) {
342
            try {
343
                $mov['cover'] = $this->releaseImage->saveImage($imdbId.'-cover', $imdb['cover'], $this->imgSavePath);
344
                if ($mov['cover'] === 0) {
345
                    Log::warning('Failed to save IMDB cover for '.$imdbId.' from URL: '.$imdb['cover']);
346
                }
347
            } catch (\Throwable $e) {
348
                Log::error('Error saving IMDB cover for '.$imdbId.': '.$e->getMessage());
349
                $mov['cover'] = 0;
350
            }
351
        }
352
353
        if ($mov['cover'] === 0 && ! empty($omdb['cover'])) {
354
            try {
355
                $mov['cover'] = $this->releaseImage->saveImage($imdbId.'-cover', $omdb['cover'], $this->imgSavePath);
356
                if ($mov['cover'] === 0) {
357
                    Log::warning('Failed to save OMDB cover for '.$imdbId.' from URL: '.$omdb['cover']);
358
                }
359
            } catch (\Throwable $e) {
360
                Log::error('Error saving OMDB cover for '.$imdbId.': '.$e->getMessage());
361
                $mov['cover'] = 0;
362
            }
363
        }
364
365
        // Backdrops.
366
        if (! empty($fanart['backdrop'])) {
367
            try {
368
                $mov['backdrop'] = $this->releaseImage->saveImage($imdbId.'-backdrop', $fanart['backdrop'], $this->imgSavePath, 1920, 1024);
369
            } catch (\Throwable $e) {
370
                Log::warning('Error saving FanartTV backdrop for '.$imdbId.': '.$e->getMessage());
371
                $mov['backdrop'] = 0;
372
            }
373
        }
374
375
        if ($mov['backdrop'] === 0 && ! empty($tmdb['backdrop'])) {
376
            try {
377
                $mov['backdrop'] = $this->releaseImage->saveImage($imdbId.'-backdrop', $tmdb['backdrop'], $this->imgSavePath, 1920, 1024);
378
            } catch (\Throwable $e) {
379
                Log::warning('Error saving TMDB backdrop for '.$imdbId.': '.$e->getMessage());
380
                $mov['backdrop'] = 0;
381
            }
382
        }
383
384
        // Banner
385
        if (! empty($fanart['banner'])) {
386
            try {
387
                $mov['banner'] = $this->releaseImage->saveImage($imdbId.'-banner', $fanart['banner'], $this->imgSavePath);
388
            } catch (\Throwable $e) {
389
                Log::warning('Error saving FanartTV banner for '.$imdbId.': '.$e->getMessage());
390
                $mov['banner'] = 0;
391
            }
392
        }
393
394
        // RottenTomatoes rating from OmdbAPI
395
        if ($omdb !== false && ! empty($omdb['rtRating'])) {
396
            $mov['rtrating'] = $omdb['rtRating'];
397
        }
398
399
        $mov['title'] = $this->setVariables($imdb['title'] ?? '', $tmdb['title'] ?? '', $trakt['title'] ?? '', $omdb['title'] ?? '');
400
        $mov['rating'] = $this->setVariables($imdb['rating'] ?? '', $tmdb['rating'] ?? '', $trakt['rating'] ?? '', $omdb['rating'] ?? '');
401
        $mov['plot'] = $this->setVariables($imdb['plot'] ?? '', $tmdb['plot'] ?? '', $trakt['overview'] ?? '', $omdb['plot'] ?? '');
402
        $mov['tagline'] = $this->setVariables($imdb['tagline'] ?? '', $tmdb['tagline'] ?? '', $trakt['tagline'] ?? '', $omdb['tagline'] ?? '');
403
        $mov['year'] = $this->setVariables($imdb['year'] ?? '', $tmdb['year'] ?? '', $trakt['year'] ?? '', $omdb['year'] ?? '');
404
        $mov['genre'] = $this->setVariables($imdb['genre'] ?? '', $tmdb['genre'] ?? '', $trakt['genres'] ?? '', $omdb['genre'] ?? '');
405
406
        if (! empty($imdb['type'])) {
407
            $mov['type'] = $imdb['type'];
408
        }
409
410
        if (! empty($imdb['director'])) {
411
            $mov['director'] = \is_array($imdb['director']) ? implode(', ', array_unique($imdb['director'])) : $imdb['director'];
412
        } elseif (! empty($omdb['director'])) {
413
            $mov['director'] = \is_array($omdb['director']) ? implode(', ', array_unique($omdb['director'])) : $omdb['director'];
414
        } elseif (! empty($tmdb['director'])) {
415
            $mov['director'] = \is_array($tmdb['director']) ? implode(', ', array_unique($tmdb['director'])) : $tmdb['director'];
416
        }
417
418
        if (! empty($imdb['actors'])) {
419
            $mov['actors'] = \is_array($imdb['actors']) ? implode(', ', array_unique($imdb['actors'])) : $imdb['actors'];
420
        } elseif (! empty($omdb['actors'])) {
421
            $mov['actors'] = \is_array($omdb['actors']) ? implode(', ', array_unique($omdb['actors'])) : $omdb['actors'];
422
        } elseif (! empty($tmdb['actors'])) {
423
            $mov['actors'] = \is_array($tmdb['actors']) ? implode(', ', array_unique($tmdb['actors'])) : $tmdb['actors'];
424
        }
425
426
        if (! empty($imdb['language'])) {
427
            $mov['language'] = \is_array($imdb['language']) ? implode(', ', array_unique($imdb['language'])) : $imdb['language'];
428
        } elseif (! empty($omdb['language']) && ! is_bool($omdb['language'])) {
429
            $mov['language'] = \is_array($omdb['language']) ? implode(', ', array_unique($omdb['language'])) : $omdb['language'];
430
        }
431
432
        if (\is_array($mov['genre'])) {
433
            $mov['genre'] = implode(', ', array_unique($mov['genre']));
434
        }
435
436
        if (\is_array($mov['type'])) {
437
            $mov['type'] = implode(', ', array_unique($mov['type']));
438
        }
439
440
        $mov['title'] = html_entity_decode($mov['title'], ENT_QUOTES, 'UTF-8');
441
442
        $mov['title'] = str_replace(['/', '\\'], '', $mov['title']);
443
        $movieID = $this->update([
444
            'actors' => html_entity_decode($mov['actors'], ENT_QUOTES, 'UTF-8'),
445
            'backdrop' => $mov['backdrop'],
446
            'cover' => $mov['cover'],
447
            'director' => html_entity_decode($mov['director'], ENT_QUOTES, 'UTF-8'),
448
            'genre' => html_entity_decode($mov['genre'], ENT_QUOTES, 'UTF-8'),
449
            'imdbid' => $mov['imdbid'],
450
            'language' => html_entity_decode($mov['language'], ENT_QUOTES, 'UTF-8'),
451
            'plot' => html_entity_decode(preg_replace('/\s+See full summary »/u', ' ', $mov['plot']), ENT_QUOTES, 'UTF-8'),
452
            'rating' => round((int) $mov['rating'], 1),
453
            'rtrating' => $mov['rtrating'] ?? 'N/A',
454
            'tagline' => html_entity_decode($mov['tagline'], ENT_QUOTES, 'UTF-8'),
455
            'title' => $mov['title'],
456
            'tmdbid' => $mov['tmdbid'],
457
            'traktid' => $mov['traktid'],
458
            'type' => html_entity_decode(ucwords(preg_replace('/[._]/', ' ', $mov['type'])), ENT_QUOTES, 'UTF-8'),
459
            'year' => $mov['year'],
460
        ]);
461
462
        // After updating, if cover flag is still 0 but file now exists (race condition), update DB.
463
        if ($mov['cover'] === 0 && $this->hasCover($imdbId)) {
464
            MovieInfo::query()->where('imdbid', $imdbId)->update(['cover' => 1]);
465
        }
466
467
        if ($this->echooutput && $this->service !== '') {
468
            PHP_EOL.$this->colorCli->headerOver('Added/updated movie: ').
0 ignored issues
show
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...
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

468
            PHP_EOL./** @scrutinizer ignore-type */ $this->colorCli->headerOver('Added/updated movie: ').
Loading history...
469
            $this->colorCli->primary(
0 ignored issues
show
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

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

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

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

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

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