FanartTvService::getBestMoviePoster()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 5
c 1
b 0
f 0
dl 0
loc 11
rs 10
cc 2
nc 2
nop 1
1
<?php
2
3
namespace App\Services;
4
5
use Illuminate\Support\Facades\Cache;
6
use Illuminate\Support\Facades\Http;
7
use Illuminate\Support\Facades\Log;
8
9
/**
10
 * FanartTV Service
11
 *
12
 * A modern service wrapper for the Fanart.TV API.
13
 * Provides methods to fetch fan art for movies and TV shows.
14
 *
15
 * API Documentation: https://fanarttv.docs.apiary.io/
16
 */
17
class FanartTvService
18
{
19
    protected const BASE_URL = 'https://webservice.fanart.tv/v3';
20
21
    protected const CACHE_TTL_HOURS = 24;
22
23
    protected string $apiKey;
24
25
    protected int $timeout;
26
27
    protected int $retryTimes;
28
29
    protected int $retryDelay;
30
31
    public function __construct(?string $apiKey = null)
32
    {
33
        $this->apiKey = $apiKey ?? (string) config('nntmux_api.fanarttv_api_key', '');
34
        $this->timeout = (int) config('nntmux_api.fanarttv_timeout', 30);
35
        $this->retryTimes = (int) config('nntmux_api.fanarttv_retry_times', 3);
36
        $this->retryDelay = (int) config('nntmux_api.fanarttv_retry_delay', 100);
37
    }
38
39
    /**
40
     * Check if the API key is configured.
41
     */
42
    public function isConfigured(): bool
43
    {
44
        return ! empty($this->apiKey);
45
    }
46
47
    /**
48
     * Make a GET request to the Fanart.TV API.
49
     *
50
     * @param  string  $endpoint  The API endpoint
51
     * @return array|null Response data or null on failure
52
     */
53
    protected function get(string $endpoint): ?array
54
    {
55
        if (! $this->isConfigured()) {
56
            Log::debug('FanartTV API key is not configured');
57
58
            return null;
59
        }
60
61
        $url = self::BASE_URL.'/'.$endpoint;
62
63
        try {
64
            $response = Http::timeout($this->timeout)
65
                ->retry($this->retryTimes, $this->retryDelay)
66
                ->withQueryParameters(['api_key' => $this->apiKey])
67
                ->get($url);
68
69
            if ($response->successful()) {
0 ignored issues
show
Bug introduced by
The method successful() does not exist on Illuminate\Http\Client\Promises\LazyPromise. ( Ignorable by Annotation )

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

69
            if ($response->/** @scrutinizer ignore-call */ successful()) {

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...
70
                $data = $response->json();
0 ignored issues
show
Bug introduced by
The method json() does not exist on Illuminate\Http\Client\Promises\LazyPromise. ( Ignorable by Annotation )

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

70
                /** @scrutinizer ignore-call */ 
71
                $data = $response->json();

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...
71
72
                // Check for API error responses
73
                if (isset($data['status']) && $data['status'] === 'error') {
74
                    Log::debug('FanartTV API returned error status', [
75
                        'endpoint' => $endpoint,
76
                        'message' => $data['error message'] ?? 'Unknown error',
77
                    ]);
78
79
                    return null;
80
                }
81
82
                return $data;
83
            }
84
85
            // Handle specific error codes
86
            if ($response->status() === 404) {
0 ignored issues
show
Bug introduced by
The method status() does not exist on Illuminate\Http\Client\Promises\LazyPromise. ( Ignorable by Annotation )

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

86
            if ($response->/** @scrutinizer ignore-call */ status() === 404) {

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...
87
                Log::debug('FanartTV: Resource not found', ['endpoint' => $endpoint]);
88
89
                return null;
90
            }
91
92
            Log::warning('FanartTV API request failed', [
93
                'endpoint' => $endpoint,
94
                'status' => $response->status(),
95
                'body' => $response->body(),
0 ignored issues
show
Bug introduced by
The method body() does not exist on Illuminate\Http\Client\Promises\LazyPromise. ( Ignorable by Annotation )

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

95
                'body' => $response->/** @scrutinizer ignore-call */ body(),

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...
96
            ]);
97
98
            return null;
99
        } catch (\Throwable $e) {
100
            Log::warning('FanartTV API request exception', [
101
                'endpoint' => $endpoint,
102
                'message' => $e->getMessage(),
103
            ]);
104
105
            return null;
106
        }
107
    }
108
109
    /**
110
     * Get fan art for a movie by IMDB ID.
111
     *
112
     * @param  string  $imdbId  IMDB ID (with or without 'tt' prefix)
113
     * @return array|null Movie art data or null on failure
114
     */
115
    public function getMovieFanArt(string $imdbId): ?array
116
    {
117
        // Ensure the ID has the 'tt' prefix
118
        $id = $this->normalizeImdbId($imdbId);
119
120
        $cacheKey = 'fanarttv_movie_'.$id;
121
122
        return Cache::remember($cacheKey, now()->addHours(self::CACHE_TTL_HOURS), function () use ($id) {
123
            return $this->get('movies/'.$id);
124
        });
125
    }
126
127
    /**
128
     * Get fan art for a TV show by TVDB ID.
129
     *
130
     * @param  int|string  $tvdbId  TVDB ID
131
     * @return array|null TV show art data or null on failure
132
     */
133
    public function getTvFanArt(int|string $tvdbId): ?array
134
    {
135
        $cacheKey = 'fanarttv_tv_'.$tvdbId;
136
137
        return Cache::remember($cacheKey, now()->addHours(self::CACHE_TTL_HOURS), function () use ($tvdbId) {
138
            return $this->get('tv/'.$tvdbId);
139
        });
140
    }
141
142
    /**
143
     * Get fan art for music by MusicBrainz ID.
144
     *
145
     * @param  string  $mbId  MusicBrainz ID
146
     * @return array|null Music art data or null on failure
147
     */
148
    public function getMusicFanArt(string $mbId): ?array
149
    {
150
        $cacheKey = 'fanarttv_music_'.$mbId;
151
152
        return Cache::remember($cacheKey, now()->addHours(self::CACHE_TTL_HOURS), function () use ($mbId) {
153
            return $this->get('music/'.$mbId);
154
        });
155
    }
156
157
    /**
158
     * Extract movie properties from Fanart.TV response.
159
     *
160
     * @param  string  $imdbId  IMDB ID (with or without 'tt' prefix)
161
     * @return array|null Array with 'cover', 'backdrop', 'banner', 'title' keys or null on failure
162
     */
163
    public function getMovieProperties(string $imdbId): ?array
164
    {
165
        $art = $this->getMovieFanArt($imdbId);
166
167
        if (empty($art)) {
168
            return null;
169
        }
170
171
        $result = [];
172
173
        // Get backdrop (preferring moviebackground, falling back to moviethumb)
174
        if (! empty($art['moviebackground'][0]['url'])) {
175
            $result['backdrop'] = $art['moviebackground'][0]['url'];
176
        } elseif (! empty($art['moviethumb'][0]['url'])) {
177
            $result['backdrop'] = $art['moviethumb'][0]['url'];
178
        }
179
180
        // Get cover/poster
181
        if (! empty($art['movieposter'][0]['url'])) {
182
            $result['cover'] = $art['movieposter'][0]['url'];
183
        }
184
185
        // Get banner
186
        if (! empty($art['moviebanner'][0]['url'])) {
187
            $result['banner'] = $art['moviebanner'][0]['url'];
188
        }
189
190
        // Only return if we have both backdrop and cover
191
        if (isset($result['backdrop'], $result['cover'])) {
192
            $result['title'] = $art['name'] ?? $imdbId;
193
194
            return $result;
195
        }
196
197
        return null;
198
    }
199
200
    /**
201
     * Extract TV show properties from Fanart.TV response.
202
     *
203
     * @param  int|string  $tvdbId  TVDB ID
204
     * @return array|null Array with 'poster', 'banner', 'background', 'title' keys or null on failure
205
     */
206
    public function getTvProperties(int|string $tvdbId): ?array
207
    {
208
        $art = $this->getTvFanArt($tvdbId);
209
210
        if (empty($art)) {
211
            return null;
212
        }
213
214
        $result = [];
215
216
        // Get poster (sort by likes)
217
        if (! empty($art['tvposter'])) {
218
            $best = collect($art['tvposter'])->sortByDesc('likes')->first();
219
            if (! empty($best['url'])) {
220
                $result['poster'] = $best['url'];
221
            }
222
        }
223
224
        // Get banner (sort by likes)
225
        if (! empty($art['tvbanner'])) {
226
            $best = collect($art['tvbanner'])->sortByDesc('likes')->first();
227
            if (! empty($best['url'])) {
228
                $result['banner'] = $best['url'];
229
            }
230
        }
231
232
        // Get background (sort by likes)
233
        if (! empty($art['showbackground'])) {
234
            $best = collect($art['showbackground'])->sortByDesc('likes')->first();
235
            if (! empty($best['url'])) {
236
                $result['background'] = $best['url'];
237
            }
238
        }
239
240
        // Get HD clearlogo (sort by likes)
241
        if (! empty($art['hdtvlogo'])) {
242
            $best = collect($art['hdtvlogo'])->sortByDesc('likes')->first();
243
            if (! empty($best['url'])) {
244
                $result['logo'] = $best['url'];
245
            }
246
        }
247
248
        if (! empty($result)) {
249
            $result['title'] = $art['name'] ?? (string) $tvdbId;
250
251
            return $result;
252
        }
253
254
        return null;
255
    }
256
257
    /**
258
     * Get the best poster URL for a TV show.
259
     *
260
     * @param  int|string  $tvdbId  TVDB ID
261
     * @return string|null Poster URL or null if not found
262
     */
263
    public function getBestTvPoster(int|string $tvdbId): ?string
264
    {
265
        $art = $this->getTvFanArt($tvdbId);
266
267
        if (empty($art['tvposter'])) {
268
            return null;
269
        }
270
271
        $best = collect($art['tvposter'])->sortByDesc('likes')->first();
272
273
        return $best['url'] ?? null;
274
    }
275
276
    /**
277
     * Get the best poster URL for a movie.
278
     *
279
     * @param  string  $imdbId  IMDB ID (with or without 'tt' prefix)
280
     * @return string|null Poster URL or null if not found
281
     */
282
    public function getBestMoviePoster(string $imdbId): ?string
283
    {
284
        $art = $this->getMovieFanArt($imdbId);
285
286
        if (empty($art['movieposter'])) {
287
            return null;
288
        }
289
290
        $best = collect($art['movieposter'])->sortByDesc('likes')->first();
291
292
        return $best['url'] ?? null;
293
    }
294
295
    /**
296
     * Normalize IMDB ID to include 'tt' prefix.
297
     */
298
    protected function normalizeImdbId(string $imdbId): string
299
    {
300
        if (str_starts_with(strtolower($imdbId), 'tt')) {
301
            return $imdbId;
302
        }
303
304
        return 'tt'.$imdbId;
305
    }
306
307
    /**
308
     * Clear cached data for a specific movie.
309
     */
310
    public function clearMovieCache(string $imdbId): bool
311
    {
312
        $id = $this->normalizeImdbId($imdbId);
313
314
        return Cache::forget('fanarttv_movie_'.$id);
315
    }
316
317
    /**
318
     * Clear cached data for a specific TV show.
319
     */
320
    public function clearTvCache(int|string $tvdbId): bool
321
    {
322
        return Cache::forget('fanarttv_tv_'.$tvdbId);
323
    }
324
}
325
326