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 GuzzleHttp\Client;
12
use GuzzleHttp\Exception\GuzzleException;
13
use Illuminate\Support\Carbon;
14
use Illuminate\Support\Facades\Cache;
15
use Illuminate\Support\Facades\File;
16
use Illuminate\Support\Facades\Log;
17
use Illuminate\Support\Str;
18
19
/**
20
 * Service class for movie data fetching and processing.
21
 */
22
class MovieService
23
{
24
    protected const MATCH_PERCENT = 75;
25
26
    protected const YEAR_MATCH_PERCENT = 80;
27
28
    protected string $currentTitle = '';
29
30
    protected string $currentYear = '';
31
32
    protected string $currentRelID = '';
33
34
    protected string $showPasswords;
35
36
    protected ReleaseImageService $releaseImage;
37
38
    protected Client $client;
39
40
    protected string $lookuplanguage;
41
42
    public FanartTvService $fanart;
43
44
    public ?string $fanartapikey;
45
46
    public ?string $omdbapikey;
47
48
    public bool $imdburl;
49
50
    public int $movieqty;
51
52
    public bool $echooutput;
53
54
    public string $imgSavePath;
55
56
    public string $service;
57
58
    public ?TraktProvider $traktTv = null;
59
60
    public ?OMDbAPI $omdbApi = null;
61
62
    protected ?string $traktcheck;
63
64
    /**
65
     * @throws \Exception
66
     */
67
    public function __construct()
68
    {
69
        $this->releaseImage = new ReleaseImageService;
70
        $this->traktcheck = config('nntmux_api.trakttv_api_key');
71
        if ($this->traktcheck !== null) {
72
            $this->traktTv = new TraktProvider;
73
        }
74
        $this->client = new Client;
75
        $this->fanartapikey = config('nntmux_api.fanarttv_api_key');
76
        $this->fanart = new FanartTvService($this->fanartapikey);
77
        $this->omdbapikey = config('nntmux_api.omdb_api_key');
78
        if ($this->omdbapikey !== null) {
79
            $this->omdbApi = new OMDbAPI($this->omdbapikey);
80
        }
81
82
        $this->lookuplanguage = Settings::settingValue('imdblanguage') !== '' ? (string) Settings::settingValue('imdblanguage') : 'en';
83
        $cacheDir = storage_path('framework/cache/imdb_cache');
84
        if (! File::isDirectory($cacheDir)) {
85
            File::makeDirectory($cacheDir, 0777, false, true);
86
        }
87
88
        $this->imdburl = (int) Settings::settingValue('imdburl') !== 0;
89
        $this->movieqty = Settings::settingValue('maximdbprocessed') !== '' ? (int) Settings::settingValue('maximdbprocessed') : 100;
90
        $this->showPasswords = app(\App\Services\Releases\ReleaseBrowseService::class)->showPasswords();
91
92
        $this->echooutput = config('nntmux.echocli');
93
        $this->imgSavePath = storage_path('covers/movies/');
94
        $this->service = '';
95
    }
96
97
    /**
98
     * Get movie info by IMDB ID.
99
     */
100
    public function getMovieInfo(?string $imdbId): ?MovieInfo
101
    {
102
        if ($imdbId === null || $imdbId === '' || $imdbId === '0000000') {
103
            return null;
104
        }
105
106
        return MovieInfo::query()->where('imdbid', $imdbId)->first();
107
    }
108
109
    /**
110
     * Get trailer using IMDB Id.
111
     *
112
     * @throws \Exception
113
     * @throws GuzzleException
114
     */
115
    public function getTrailer(int $imdbId): string|false
116
    {
117
        $trailer = MovieInfo::query()->where('imdbid', $imdbId)->where('trailer', '<>', '')->first(['trailer']);
118
        if ($trailer !== null) {
119
            return $trailer['trailer'];
120
        }
121
122
        if ($this->traktcheck !== null) {
123
            $data = $this->traktTv->client->getMovieSummary('tt'.$imdbId, 'full');
124
            if (($data !== false) && ! empty($data['trailer'])) {
125
                return $data['trailer'];
126
            }
127
        }
128
129
        $trailer = imdb_trailers($imdbId);
130
        if ($trailer) {
131
            MovieInfo::query()->where('imdbid', $imdbId)->update(['trailer' => $trailer]);
132
133
            return $trailer;
134
        }
135
136
        return false;
137
    }
138
139
    /**
140
     * Parse trakt info, insert into DB.
141
     */
142
    public function parseTraktTv(array &$data): mixed
143
    {
144
        if (empty($data['ids']['imdb'])) {
145
            return false;
146
        }
147
148
        if (! empty($data['trailer'])) {
149
            $data['trailer'] = str_ireplace(
150
                ['watch?v=', 'http://'],
151
                ['embed/', 'https://'],
152
                $data['trailer']
153
            );
154
        }
155
        $imdbId = (str_starts_with($data['ids']['imdb'], 'tt')) ? substr($data['ids']['imdb'], 2) : $data['ids']['imdb'];
156
        $cover = 0;
157
        if (File::isFile($this->imgSavePath.$imdbId.'-cover.jpg')) {
158
            $cover = 1;
159
        }
160
161
        return $this->update([
162
            'genre' => implode(', ', $data['genres']),
163
            'imdbid' => $this->checkTraktValue($imdbId),
164
            'language' => $this->checkTraktValue($data['language']),
165
            'plot' => $this->checkTraktValue($data['overview']),
166
            'rating' => $this->checkTraktValue($data['rating']),
167
            'tagline' => $this->checkTraktValue($data['tagline']),
168
            'title' => $this->checkTraktValue($data['title']),
169
            'tmdbid' => $this->checkTraktValue($data['ids']['tmdb']),
170
            'traktid' => $this->checkTraktValue($data['ids']['trakt']),
171
            'trailer' => $this->checkTraktValue($data['trailer']),
172
            'cover' => $cover,
173
            'year' => $this->checkTraktValue($data['year']),
174
        ]);
175
    }
176
177
    private function checkTraktValue(mixed $value): mixed
178
    {
179
        if (\is_array($value) && ! empty($value)) {
180
            $temp = '';
181
            foreach ($value as $val) {
182
                if (! is_array($val) && ! is_object($val)) {
183
                    $temp .= $val;
184
                }
185
            }
186
            $value = $temp;
187
        }
188
189
        return ! empty($value) ? $value : '';
190
    }
191
192
    /**
193
     * Get array of column keys, for inserting / updating.
194
     */
195
    public function getColumnKeys(): array
196
    {
197
        return [
198
            'actors', 'backdrop', 'cover', 'director', 'genre', 'imdbid', 'language',
199
            'plot', 'rating', 'rtrating', 'tagline', 'title', 'tmdbid', 'traktid', 'trailer', 'type', 'year',
200
        ];
201
    }
202
203
    /**
204
     * Choose the first non-empty variable from up to five inputs.
205
     */
206
    protected function setVariables(string|array $variable1, string|array $variable2, string|array $variable3, string|array $variable4, string|array $variable5 = ''): array|string
207
    {
208
        if (! empty($variable1)) {
209
            return $variable1;
210
        }
211
        if (! empty($variable2)) {
212
            return $variable2;
213
        }
214
        if (! empty($variable3)) {
215
            return $variable3;
216
        }
217
        if (! empty($variable4)) {
218
            return $variable4;
219
        }
220
        if (! empty($variable5)) {
221
            return $variable5;
222
        }
223
224
        return '';
225
    }
226
227
    /**
228
     * Update movie on movie-edit page.
229
     */
230
    public function update(array $values): bool
231
    {
232
        if (! count($values)) {
233
            return false;
234
        }
235
236
        $query = [];
237
        $onDuplicateKey = ['created_at' => now()];
238
        $found = 0;
239
        foreach ($values as $key => $value) {
240
            if (! empty($value)) {
241
                $found++;
242
                if (\in_array($key, ['genre', 'language'], false)) {
243
                    $value = substr($value, 0, 64);
244
                }
245
                $query += [$key => $value];
246
                $onDuplicateKey += [$key => $value];
247
            }
248
        }
249
        if (! $found) {
250
            return false;
251
        }
252
        foreach ($query as $key => $value) {
253
            $query[$key] = rtrim($value, ', ');
254
        }
255
256
        MovieInfo::upsert($query, ['imdbid'], $onDuplicateKey);
257
258
        // Always attempt to fetch a missing cover if imdbid present and cover not provided.
259
        if (! empty($query['imdbid'])) {
260
            $imdbIdForCover = $query['imdbid'];
261
            $coverProvided = array_key_exists('cover', $values) && ! empty($values['cover']);
262
            if (! $coverProvided && ! $this->hasCover($imdbIdForCover)) {
263
                if ($this->fetchAndSaveCoverOnly($imdbIdForCover)) {
264
                    MovieInfo::query()->where('imdbid', $imdbIdForCover)->update(['cover' => 1]);
265
                }
266
            }
267
        }
268
269
        return true;
270
    }
271
272
    /**
273
     * Fetch IMDB/TMDB/TRAKT/OMDB/iTunes info for the movie.
274
     *
275
     * @throws \Exception
276
     */
277
    public function updateMovieInfo(string $imdbId): bool
278
    {
279
        if ($this->echooutput && $this->service !== '') {
280
            cli()->primary('Fetching IMDB info from TMDB/IMDB/Trakt/OMDB/iTunes using IMDB id: '.$imdbId);
281
        }
282
283
        // Check TMDB for IMDB info.
284
        $tmdb = $this->fetchTMDBProperties($imdbId);
285
286
        // Check IMDB for movie info.
287
        $imdb = $this->fetchIMDBProperties($imdbId);
288
289
        // Check TRAKT for movie info
290
        $trakt = $this->fetchTraktTVProperties($imdbId);
291
292
        // Check OMDb for movie info
293
        $omdb = $this->fetchOmdbAPIProperties($imdbId);
294
295
        if (! $imdb && ! $tmdb && ! $trakt && ! $omdb) {
0 ignored issues
show
introduced by
The condition $imdb is always false.
Loading history...
296
            return false;
297
        }
298
299
        // Check FanArt.tv for cover and background images.
300
        $fanart = $this->fetchFanartTVProperties($imdbId);
301
302
        $mov = [];
303
304
        $mov['cover'] = $mov['backdrop'] = $mov['banner'] = 0;
305
        $mov['type'] = $mov['director'] = $mov['actors'] = $mov['language'] = '';
306
307
        $mov['imdbid'] = $imdbId;
308
        $mov['tmdbid'] = (! isset($tmdb['tmdbid']) || $tmdb['tmdbid'] === '') ? 0 : $tmdb['tmdbid'];
309
        $mov['traktid'] = (! isset($trakt['id']) || $trakt['id'] === '') ? 0 : $trakt['id'];
310
311
        // Prefer Fanart.tv cover over TMDB,TMDB over IMDB,IMDB over OMDB and OMDB over iTunes.
312
        if (! empty($fanart['cover'])) {
313
            try {
314
                $mov['cover'] = $this->releaseImage->saveImage($imdbId.'-cover', $fanart['cover'], $this->imgSavePath);
315
                if ($mov['cover'] === 0) {
316
                    Log::warning('Failed to save FanartTV cover for '.$imdbId.' from URL: '.$fanart['cover']);
317
                }
318
            } catch (\Throwable $e) {
319
                Log::error('Error saving FanartTV cover for '.$imdbId.': '.$e->getMessage());
320
                $mov['cover'] = 0;
321
            }
322
        }
323
324
        if ($mov['cover'] === 0 && ! empty($tmdb['cover'])) {
325
            try {
326
                $mov['cover'] = $this->releaseImage->saveImage($imdbId.'-cover', $tmdb['cover'], $this->imgSavePath);
327
                if ($mov['cover'] === 0) {
328
                    Log::warning('Failed to save TMDB cover for '.$imdbId.' from URL: '.$tmdb['cover']);
329
                }
330
            } catch (\Throwable $e) {
331
                Log::error('Error saving TMDB cover for '.$imdbId.': '.$e->getMessage());
332
                $mov['cover'] = 0;
333
            }
334
        }
335
336
        if ($mov['cover'] === 0 && ! empty($imdb['cover'])) {
337
            try {
338
                $mov['cover'] = $this->releaseImage->saveImage($imdbId.'-cover', $imdb['cover'], $this->imgSavePath);
339
                if ($mov['cover'] === 0) {
340
                    Log::warning('Failed to save IMDB cover for '.$imdbId.' from URL: '.$imdb['cover']);
341
                }
342
            } catch (\Throwable $e) {
343
                Log::error('Error saving IMDB cover for '.$imdbId.': '.$e->getMessage());
344
                $mov['cover'] = 0;
345
            }
346
        }
347
348
        if ($mov['cover'] === 0 && ! empty($omdb['cover'])) {
349
            try {
350
                $mov['cover'] = $this->releaseImage->saveImage($imdbId.'-cover', $omdb['cover'], $this->imgSavePath);
351
                if ($mov['cover'] === 0) {
352
                    Log::warning('Failed to save OMDB cover for '.$imdbId.' from URL: '.$omdb['cover']);
353
                }
354
            } catch (\Throwable $e) {
355
                Log::error('Error saving OMDB cover for '.$imdbId.': '.$e->getMessage());
356
                $mov['cover'] = 0;
357
            }
358
        }
359
360
        // Backdrops.
361
        if (! empty($fanart['backdrop'])) {
362
            try {
363
                $mov['backdrop'] = $this->releaseImage->saveImage($imdbId.'-backdrop', $fanart['backdrop'], $this->imgSavePath, 1920, 1024);
364
            } catch (\Throwable $e) {
365
                Log::warning('Error saving FanartTV backdrop for '.$imdbId.': '.$e->getMessage());
366
                $mov['backdrop'] = 0;
367
            }
368
        }
369
370
        if ($mov['backdrop'] === 0 && ! empty($tmdb['backdrop'])) {
371
            try {
372
                $mov['backdrop'] = $this->releaseImage->saveImage($imdbId.'-backdrop', $tmdb['backdrop'], $this->imgSavePath, 1920, 1024);
373
            } catch (\Throwable $e) {
374
                Log::warning('Error saving TMDB backdrop for '.$imdbId.': '.$e->getMessage());
375
                $mov['backdrop'] = 0;
376
            }
377
        }
378
379
        // Banner
380
        if (! empty($fanart['banner'])) {
381
            try {
382
                $mov['banner'] = $this->releaseImage->saveImage($imdbId.'-banner', $fanart['banner'], $this->imgSavePath);
383
            } catch (\Throwable $e) {
384
                Log::warning('Error saving FanartTV banner for '.$imdbId.': '.$e->getMessage());
385
                $mov['banner'] = 0;
386
            }
387
        }
388
389
        // RottenTomatoes rating from OmdbAPI
390
        if ($omdb !== false && ! empty($omdb['rtRating'])) {
391
            $mov['rtrating'] = $omdb['rtRating'];
392
        }
393
394
        $mov['title'] = $this->setVariables($imdb['title'] ?? '', $tmdb['title'] ?? '', $trakt['title'] ?? '', $omdb['title'] ?? '');
395
        $mov['rating'] = $this->setVariables($imdb['rating'] ?? '', $tmdb['rating'] ?? '', $trakt['rating'] ?? '', $omdb['rating'] ?? '');
396
        $mov['plot'] = $this->setVariables($imdb['plot'] ?? '', $tmdb['plot'] ?? '', $trakt['overview'] ?? '', $omdb['plot'] ?? '');
397
        $mov['tagline'] = $this->setVariables($imdb['tagline'] ?? '', $tmdb['tagline'] ?? '', $trakt['tagline'] ?? '', $omdb['tagline'] ?? '');
398
        $mov['year'] = $this->setVariables($imdb['year'] ?? '', $tmdb['year'] ?? '', $trakt['year'] ?? '', $omdb['year'] ?? '');
399
        $mov['genre'] = $this->setVariables($imdb['genre'] ?? '', $tmdb['genre'] ?? '', $trakt['genres'] ?? '', $omdb['genre'] ?? '');
400
401
        if (! empty($imdb['type'])) {
402
            $mov['type'] = $imdb['type'];
403
        }
404
405
        if (! empty($imdb['director'])) {
406
            $mov['director'] = \is_array($imdb['director']) ? implode(', ', array_unique($imdb['director'])) : $imdb['director'];
407
        } elseif (! empty($omdb['director'])) {
408
            $mov['director'] = \is_array($omdb['director']) ? implode(', ', array_unique($omdb['director'])) : $omdb['director'];
409
        } elseif (! empty($tmdb['director'])) {
410
            $mov['director'] = \is_array($tmdb['director']) ? implode(', ', array_unique($tmdb['director'])) : $tmdb['director'];
411
        }
412
413
        if (! empty($imdb['actors'])) {
414
            $mov['actors'] = \is_array($imdb['actors']) ? implode(', ', array_unique($imdb['actors'])) : $imdb['actors'];
415
        } elseif (! empty($omdb['actors'])) {
416
            $mov['actors'] = \is_array($omdb['actors']) ? implode(', ', array_unique($omdb['actors'])) : $omdb['actors'];
417
        } elseif (! empty($tmdb['actors'])) {
418
            $mov['actors'] = \is_array($tmdb['actors']) ? implode(', ', array_unique($tmdb['actors'])) : $tmdb['actors'];
419
        }
420
421
        if (! empty($imdb['language'])) {
422
            $mov['language'] = \is_array($imdb['language']) ? implode(', ', array_unique($imdb['language'])) : $imdb['language'];
423
        } elseif (! empty($omdb['language']) && ! is_bool($omdb['language'])) {
424
            $mov['language'] = \is_array($omdb['language']) ? implode(', ', array_unique($omdb['language'])) : $omdb['language'];
425
        }
426
427
        if (\is_array($mov['genre'])) {
428
            $mov['genre'] = implode(', ', array_unique($mov['genre']));
429
        }
430
431
        if (\is_array($mov['type'])) {
432
            $mov['type'] = implode(', ', array_unique($mov['type']));
433
        }
434
435
        $mov['title'] = html_entity_decode($mov['title'], ENT_QUOTES, 'UTF-8');
436
437
        $mov['title'] = str_replace(['/', '\\'], '', $mov['title']);
438
        $movieID = $this->update([
439
            'actors' => html_entity_decode($mov['actors'], ENT_QUOTES, 'UTF-8'),
440
            'backdrop' => $mov['backdrop'],
441
            'cover' => $mov['cover'],
442
            'director' => html_entity_decode($mov['director'], ENT_QUOTES, 'UTF-8'),
443
            'genre' => html_entity_decode($mov['genre'], ENT_QUOTES, 'UTF-8'),
444
            'imdbid' => $mov['imdbid'],
445
            'language' => html_entity_decode($mov['language'], ENT_QUOTES, 'UTF-8'),
446
            'plot' => html_entity_decode(preg_replace('/\s+See full summary »/u', ' ', $mov['plot']), ENT_QUOTES, 'UTF-8'),
447
            'rating' => round((int) $mov['rating'], 1),
448
            'rtrating' => $mov['rtrating'] ?? 'N/A',
449
            'tagline' => html_entity_decode($mov['tagline'], ENT_QUOTES, 'UTF-8'),
450
            'title' => $mov['title'],
451
            'tmdbid' => $mov['tmdbid'],
452
            'traktid' => $mov['traktid'],
453
            'type' => html_entity_decode(ucwords(preg_replace('/[._]/', ' ', $mov['type'])), ENT_QUOTES, 'UTF-8'),
454
            'year' => $mov['year'],
455
        ]);
456
457
        // After updating, if cover flag is still 0 but file now exists (race condition), update DB.
458
        if ($mov['cover'] === 0 && $this->hasCover($imdbId)) {
459
            MovieInfo::query()->where('imdbid', $imdbId)->update(['cover' => 1]);
460
        }
461
462
        if ($this->echooutput && $this->service !== '') {
463
            PHP_EOL.cli()->headerOver('Added/updated movie: ').
464
            cli()->primary(
465
                $mov['title'].
466
                ' ('.
467
                $mov['year'].
468
                ') - '.
469
                $mov['imdbid']
470
            );
471
        }
472
473
        return $movieID;
474
    }
475
476
    /**
477
     * Fetch FanArt.tv backdrop / cover / title.
478
     */
479
    protected function fetchFanartTVProperties(string $imdbId): false|array
480
    {
481
        if (! $this->fanart->isConfigured()) {
482
            return false;
483
        }
484
485
        try {
486
            $result = $this->fanart->getMovieProperties($imdbId);
487
488
            if ($result !== null) {
489
                if ($this->echooutput) {
490
                    cli()->info('Fanart found '.$result['title']);
491
                }
492
493
                return $result;
494
            }
495
        } catch (\Throwable $e) {
496
            Log::warning('FanartTV API error for '.$imdbId.': '.$e->getMessage());
497
        }
498
499
        return false;
500
    }
501
502
    /**
503
     * Fetch movie information from TMDB using an IMDB ID.
504
     */
505
    public function fetchTMDBProperties(string $imdbId, bool $text = false): array|false
506
    {
507
        $lookupId = $text === false && (strlen($imdbId) === 7 || strlen($imdbId) === 8) ? 'tt'.$imdbId : $imdbId;
508
509
        $cacheKey = 'tmdb_movie_'.md5($lookupId);
510
        $expiresAt = now()->addDays(7);
511
512
        if (Cache::has($cacheKey)) {
513
            return Cache::get($cacheKey);
514
        }
515
516
        try {
517
            $tmdbClient = app(TmdbClient::class);
518
519
            if (! $tmdbClient->isConfigured()) {
520
                return false;
521
            }
522
523
            $tmdbLookup = $tmdbClient->getMovie($lookupId, ['credits']);
524
525
            if ($tmdbLookup === null || empty($tmdbLookup)) {
526
                Cache::put($cacheKey, false, $expiresAt);
527
528
                return false;
529
            }
530
531
            $title = TmdbClient::getString($tmdbLookup, 'title');
532
            if ($this->currentTitle !== '' && ! empty($title)) {
533
                similar_text($this->currentTitle, $title, $percent);
534
                if ($percent < self::MATCH_PERCENT) {
535
                    Cache::put($cacheKey, false, $expiresAt);
536
537
                    return false;
538
                }
539
            }
540
541
            $releaseDate = TmdbClient::getString($tmdbLookup, 'release_date');
542
            if ($this->currentYear !== '' && ! empty($releaseDate)) {
543
                $tmdbYear = Carbon::parse($releaseDate)->year;
544
545
                similar_text($this->currentYear, (string) $tmdbYear, $percent);
546
                if ($percent < self::YEAR_MATCH_PERCENT) {
547
                    Cache::put($cacheKey, false, $expiresAt);
548
549
                    return false;
550
                }
551
            }
552
553
            $imdbIdFromResponse = TmdbClient::getString($tmdbLookup, 'imdb_id');
554
            $ret = [
555
                'title' => $title,
556
                'tmdbid' => TmdbClient::getInt($tmdbLookup, 'id'),
557
                'imdbid' => str_replace('tt', '', $imdbIdFromResponse),
558
                'rating' => '',
559
                'actors' => '',
560
                'director' => '',
561
                'plot' => TmdbClient::getString($tmdbLookup, 'overview'),
562
                'tagline' => TmdbClient::getString($tmdbLookup, 'tagline'),
563
                'year' => '',
564
                'genre' => '',
565
                'cover' => '',
566
                'backdrop' => '',
567
            ];
568
569
            $vote = TmdbClient::getFloat($tmdbLookup, 'vote_average');
570
            if ($vote > 0) {
571
                $ret['rating'] = $vote;
572
            }
573
574
            $credits = TmdbClient::getArray($tmdbLookup, 'credits');
575
            $cast = TmdbClient::getArray($credits, 'cast');
576
            if (! empty($cast)) {
577
                $actors = [];
578
                foreach ($cast as $member) {
579
                    if (is_array($member) && ! empty($member['name'])) {
580
                        $actors[] = $member['name'];
581
                    }
582
                }
583
                if (! empty($actors)) {
584
                    $ret['actors'] = $actors;
585
                }
586
            }
587
588
            $crew = TmdbClient::getArray($credits, 'crew');
589
            foreach ($crew as $crewMember) {
590
                if (! is_array($crewMember)) {
591
                    continue;
592
                }
593
                $department = TmdbClient::getString($crewMember, 'department');
594
                $job = TmdbClient::getString($crewMember, 'job');
595
                if ($department === 'Directing' && $job === 'Director') {
596
                    $ret['director'] = TmdbClient::getString($crewMember, 'name');
597
                    break;
598
                }
599
            }
600
601
            if (! empty($releaseDate)) {
602
                $ret['year'] = Carbon::parse($releaseDate)->year;
603
            }
604
605
            $genresa = TmdbClient::getArray($tmdbLookup, 'genres');
606
            if (! empty($genresa)) {
607
                $genres = [];
608
                foreach ($genresa as $genre) {
609
                    if (is_array($genre) && ! empty($genre['name'])) {
610
                        $genres[] = $genre['name'];
611
                    }
612
                }
613
                if (! empty($genres)) {
614
                    $ret['genre'] = $genres;
615
                }
616
            }
617
618
            $posterPath = TmdbClient::getString($tmdbLookup, 'poster_path');
619
            if (! empty($posterPath)) {
620
                $ret['cover'] = 'https://image.tmdb.org/t/p/original'.$posterPath;
621
            }
622
623
            $backdropPath = TmdbClient::getString($tmdbLookup, 'backdrop_path');
624
            if (! empty($backdropPath)) {
625
                $ret['backdrop'] = 'https://image.tmdb.org/t/p/original'.$backdropPath;
626
            }
627
628
            if ($this->echooutput) {
629
                cli()->info('TMDb found '.$ret['title']);
630
            }
631
632
            Cache::put($cacheKey, $ret, $expiresAt);
633
634
            return $ret;
635
636
        } catch (\Throwable $e) {
637
            Log::warning('TMDB API error for '.$lookupId.': '.$e->getMessage());
638
            Cache::put($cacheKey, false, now()->addHours(6));
639
640
            return false;
641
        }
642
    }
643
644
    /**
645
     * Fetch movie information from IMDB.
646
     */
647
    public function fetchIMDBProperties(string $imdbId): array|false
648
    {
649
        $cacheKey = 'imdb_movie_'.md5($imdbId);
650
        $expiresAt = now()->addDays(7);
651
        if (Cache::has($cacheKey)) {
652
            return Cache::get($cacheKey);
653
        }
654
        try {
655
            $scraper = app(ImdbScraper::class);
656
            $scraped = $scraper->fetchById($imdbId);
657
            if ($scraped === false || empty($scraped['title'])) {
658
                Cache::put($cacheKey, false, now()->addHours(6));
659
660
                return false;
661
            }
662
            if (! empty($this->currentTitle)) {
663
                similar_text($this->currentTitle, $scraped['title'], $percent);
664
                if ($percent < self::MATCH_PERCENT) {
665
                    Cache::put($cacheKey, false, now()->addHours(6));
666
667
                    return false;
668
                }
669
                if (! empty($this->currentYear) && ! empty($scraped['year'])) {
670
                    similar_text($this->currentYear, $scraped['year'], $yearPercent);
671
                    if ($yearPercent < self::YEAR_MATCH_PERCENT) {
672
                        Cache::put($cacheKey, false, now()->addHours(6));
673
674
                        return false;
675
                    }
676
                }
677
            }
678
            Cache::put($cacheKey, $scraped, $expiresAt);
679
            if ($this->echooutput) {
680
                cli()->info('IMDb scraped '.$scraped['title']);
681
            }
682
683
            return $scraped;
684
        } catch (\Throwable $e) {
685
            Log::warning('IMDb scrape error for '.$imdbId.': '.$e->getMessage());
686
            Cache::put($cacheKey, false, now()->addHours(6));
687
688
            return false;
689
        }
690
    }
691
692
    /**
693
     * Fetch movie information from Trakt.tv using IMDB ID.
694
     *
695
     * @throws GuzzleException
696
     */
697
    public function fetchTraktTVProperties(string $imdbId): array|false
698
    {
699
        if ($this->traktcheck === null) {
700
            return false;
701
        }
702
703
        $cacheKey = 'trakt_movie_'.md5($imdbId);
704
        $expiresAt = now()->addDays(7);
705
706
        if (Cache::has($cacheKey)) {
707
            return Cache::get($cacheKey);
708
        }
709
710
        try {
711
            $resp = $this->traktTv->client->getMovieSummary('tt'.$imdbId, 'full');
712
713
            if ($resp === false || empty($resp['title'])) {
714
                Cache::put($cacheKey, false, now()->addHours(6));
715
716
                return false;
717
            }
718
719
            if (! empty($this->currentTitle)) {
720
                similar_text($this->currentTitle, $resp['title'], $percent);
721
                if ($percent < self::MATCH_PERCENT) {
722
                    Cache::put($cacheKey, false, now()->addHours(6));
723
724
                    return false;
725
                }
726
            }
727
728
            if (! empty($this->currentYear) && ! empty($resp['year'])) {
729
                similar_text($this->currentYear, $resp['year'], $percent);
730
                if ($percent < self::YEAR_MATCH_PERCENT) {
731
                    Cache::put($cacheKey, false, now()->addHours(6));
732
733
                    return false;
734
                }
735
            }
736
737
            $movieData = [
738
                'id' => $resp['ids']['trakt'] ?? null,
739
                'title' => $resp['title'],
740
                'overview' => $resp['overview'] ?? '',
741
                'tagline' => $resp['tagline'] ?? '',
742
                'year' => $resp['year'] ?? '',
743
                'genres' => $resp['genres'] ?? '',
744
                'rating' => $resp['rating'] ?? '',
745
                'votes' => $resp['votes'] ?? 0,
746
                'language' => $resp['language'] ?? '',
747
                'runtime' => $resp['runtime'] ?? 0,
748
                'trailer' => $resp['trailer'] ?? '',
749
            ];
750
751
            if ($this->echooutput) {
752
                cli()->info('Trakt found '.$movieData['title']);
753
            }
754
755
            Cache::put($cacheKey, $movieData, $expiresAt);
756
757
            return $movieData;
758
759
        } catch (\Throwable $e) {
760
            Log::warning('Trakt API error for '.$imdbId.': '.$e->getMessage());
761
            Cache::put($cacheKey, false, now()->addHours(6));
762
763
            return false;
764
        }
765
    }
766
767
    /**
768
     * Fetch movie information from OMDB API using IMDB ID.
769
     */
770
    public function fetchOmdbAPIProperties(string $imdbId): array|false
771
    {
772
        if ($this->omdbapikey === null) {
773
            return false;
774
        }
775
776
        $cacheKey = 'omdb_movie_'.md5($imdbId);
777
        $expiresAt = now()->addDays(7);
778
779
        if (Cache::has($cacheKey)) {
780
            return Cache::get($cacheKey);
781
        }
782
783
        try {
784
            $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

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

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

928
            ->whereBetween(/** @scrutinizer ignore-type */ 'categories_id', [Category::MOVIE_ROOT, Category::MOVIE_OTHER])
Loading history...
929
            ->whereNull('imdbid');
930
931
        if ($groupID !== '') {
932
            $query->where('groups_id', $groupID);
933
        }
934
935
        if ($guidChar !== '') {
936
            $query->where('leftguid', $guidChar);
937
        }
938
939
        if ((int) $lookupIMDB === 2) {
940
            $query->where('isrenamed', '=', 1);
941
        }
942
943
        $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

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

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

1198
            $end = Carbon::createFromFormat('Y', $this->currentYear)->/** @scrutinizer ignore-call */ addYears(2)->year;
Loading history...
1199
            $query->whereBetween('year', [$start, $end]);
0 ignored issues
show
Bug introduced by
'year' of type string is incompatible with the type Illuminate\Database\Eloq...\Database\Query\Builder expected by parameter $column of Illuminate\Database\Query\Builder::whereBetween(). ( Ignorable by Annotation )

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

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