ApiController::group()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 11
rs 10
c 0
b 0
f 0
cc 3
nc 3
nop 1
1
<?php
2
3
namespace App\Http\Controllers\Api;
4
5
use App\Events\UserAccessedApi;
6
use App\Http\Controllers\BasePageController;
7
use App\Models\Category;
8
use App\Models\Release;
9
use App\Models\ReleaseNfo;
10
use App\Models\Settings;
11
use App\Models\UsenetGroup;
12
use App\Models\User;
13
use App\Models\UserDownload;
14
use App\Models\UserRequest;
15
use Blacklight\NZB;
16
use Blacklight\Releases;
17
use Blacklight\utility\Utility;
18
use Illuminate\Contracts\Foundation\Application;
19
use Illuminate\Http\RedirectResponse;
20
use Illuminate\Http\Request;
21
use Illuminate\Routing\Redirector;
22
use Illuminate\Support\Carbon;
23
use Illuminate\Support\Facades\File;
24
use Illuminate\Support\Facades\Log;
25
use Illuminate\Support\Str;
26
use Symfony\Component\HttpFoundation\StreamedResponse;
27
28
class ApiController extends BasePageController
29
{
30
    private string $type;
31
32
    /**
33
     * @return Application|\Illuminate\Foundation\Application|RedirectResponse|Redirector|StreamedResponse|void
34
     *
35
     * @throws \Throwable
36
     */
37
    public function api(Request $request)
38
    {
39
        // API functions.
40
        $function = 's';
41
        if ($request->has('t')) {
42
            switch ($request->input('t')) {
43
                case 'd':
44
                case 'details':
45
                    $function = 'd';
46
                    break;
47
                case 'g':
48
                case 'get':
49
                    $function = 'g';
50
                    break;
51
                case 's':
52
                case 'search':
53
                    break;
54
                case 'c':
55
                case 'caps':
56
                    $function = 'c';
57
                    break;
58
                case 'tv':
59
                case 'tvsearch':
60
                    $function = 'tv';
61
                    break;
62
                case 'm':
63
                case 'movie':
64
                    $function = 'm';
65
                    break;
66
                case 'gn':
67
                case 'n':
68
                case 'nfo':
69
                case 'info':
70
                    $function = 'n';
71
                    break;
72
                case 'nzbadd':
73
                    $function = 'nzbAdd';
74
                    break;
75
                default:
76
                    return Utility::showApiError(202, 'No such function ('.$request->input('t').')');
77
            }
78
        } else {
79
            return Utility::showApiError(200, 'Missing parameter (t)');
80
        }
81
82
        $uid = $apiKey = $oldestGrabTime = $thisOldestTime = '';
83
        $res = $catExclusions = [];
84
        $maxRequests = $thisRequests = $maxDownloads = $grabs = 0;
85
86
        // Page is accessible only by the apikey
87
88
        if ($function !== 'c' && $function !== 'r') {
89
            if ($request->missing('apikey') || ($request->has('apikey') && empty($request->input('apikey')))) {
90
                return Utility::showApiError(200, 'Missing parameter (apikey)');
91
            }
92
93
            $apiKey = $request->input('apikey');
94
            $res = User::getByRssToken($apiKey);
95
            if ($res === null) {
96
                return Utility::showApiError(100, 'Incorrect user credentials (wrong API key)');
97
            }
98
99
            if ($res->hasRole('Disabled')) {
100
                return Utility::showApiError(101);
101
            }
102
103
            $uid = $res->id;
104
            $catExclusions = User::getCategoryExclusionForApi($request);
105
            $maxRequests = $res->role->apirequests;
0 ignored issues
show
Bug introduced by
The property apirequests does not seem to exist on Spatie\Permission\Models\Role.
Loading history...
106
            $maxDownloads = $res->role->downloadrequests;
0 ignored issues
show
Bug introduced by
The property downloadrequests does not seem to exist on Spatie\Permission\Models\Role.
Loading history...
107
            $time = UserRequest::whereUsersId($uid)->min('timestamp');
108
            $thisOldestTime = $time !== null ? Carbon::createFromTimeString($time)->toRfc2822String() : '';
109
            $grabTime = UserDownload::whereUsersId($uid)->min('timestamp');
110
            $oldestGrabTime = $grabTime !== null ? Carbon::createFromTimeString($grabTime)->toRfc2822String() : '';
111
        }
112
113
        // Record user access to the api, if its been called by a user (i.e. capabilities request do not require a user to be logged in or key provided).
114
        if ($uid !== '') {
115
            event(new UserAccessedApi($res));
116
            $thisRequests = UserRequest::getApiRequests($uid);
0 ignored issues
show
Bug introduced by
It seems like $uid can also be of type string; however, parameter $userID of App\Models\UserRequest::getApiRequests() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

116
            $thisRequests = UserRequest::getApiRequests(/** @scrutinizer ignore-type */ $uid);
Loading history...
117
            $grabs = UserDownload::getDownloadRequests($uid);
0 ignored issues
show
Bug introduced by
It seems like $uid can also be of type string; however, parameter $userID of App\Models\UserDownload::getDownloadRequests() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

117
            $grabs = UserDownload::getDownloadRequests(/** @scrutinizer ignore-type */ $uid);
Loading history...
118
            if ($thisRequests > $maxRequests) {
119
                return Utility::showApiError(500, 'Request limit reached ('.$thisRequests.'/'.$maxRequests.')');
120
            }
121
        }
122
123
        $releases = new Releases;
124
125
        // Set Query Parameters based on Request objects
126
        $outputXML = ! ($request->has('o') && $request->input('o') === 'json');
127
        $minSize = $request->has('minsize') && $request->input('minsize') > 0 ? $request->input('minsize') : 0;
128
        $offset = $this->offset($request);
129
130
        // Set API Parameters based on Request objects
131
        $params['extended'] = $request->has('extended') && (int) $request->input('extended') === 1 ? '1' : '0';
0 ignored issues
show
Comprehensibility Best Practice introduced by
$params was never initialized. Although not strictly required by PHP, it is generally a good practice to add $params = array(); before regardless.
Loading history...
132
        $params['del'] = $request->has('del') && (int) $request->input('del') === 1 ? '1' : '0';
133
        $params['uid'] = $uid;
134
        $params['token'] = $apiKey;
135
        $params['apilimit'] = $maxRequests;
136
        $params['requests'] = $thisRequests;
137
        $params['downloadlimit'] = $maxDownloads;
138
        $params['grabs'] = $grabs;
139
        $params['oldestapi'] = $thisOldestTime;
140
        $params['oldestgrab'] = $oldestGrabTime;
141
142
        switch ($function) {
143
            // Search releases.
144
            case 's':
145
                $this->verifyEmptyParameter($request, 'q');
146
                $maxAge = $this->maxAge($request);
147
                $groupName = $this->group($request);
148
                UserRequest::addApiRequest($apiKey, $request->getRequestUri());
149
                $categoryID = $this->categoryID($request);
150
                $limit = $this->limit($request);
151
                $searchArr = [
152
                    'searchname' => $request->input('q') ?? -1,
153
                    'name' => -1,
154
                    'fromname' => -1,
155
                    'filename' => -1,
156
                ];
157
158
                if ($request->has('q')) {
159
                    $relData = $releases->search(
160
                        $searchArr,
161
                        $groupName,
162
                        -1,
163
                        -1,
164
                        -1,
165
                        -1,
166
                        $offset,
167
                        $limit,
168
                        '',
169
                        $maxAge,
0 ignored issues
show
Bug introduced by
It seems like $maxAge can also be of type Illuminate\Http\Response; however, parameter $maxAge of Blacklight\Releases::search() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

169
                        /** @scrutinizer ignore-type */ $maxAge,
Loading history...
170
                        $catExclusions,
171
                        'basic',
172
                        $categoryID,
173
                        $minSize
174
                    );
175
                } else {
176
                    $relData = $releases->getBrowseRange(
177
                        1,
178
                        $categoryID,
179
                        $offset,
180
                        $limit,
181
                        '',
182
                        $maxAge,
0 ignored issues
show
Bug introduced by
It seems like $maxAge can also be of type Illuminate\Http\Response; however, parameter $maxAge of Blacklight\Releases::getBrowseRange() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

182
                        /** @scrutinizer ignore-type */ $maxAge,
Loading history...
183
                        $catExclusions,
184
                        $groupName,
185
                        $minSize
186
                    );
187
                }
188
                $this->output($relData, $params, $outputXML, $offset, 'api');
189
                break;
190
                // Search tv releases.
191
            case 'tv':
192
                $this->verifyEmptyParameter($request, 'q');
193
                $this->verifyEmptyParameter($request, 'vid');
194
                $this->verifyEmptyParameter($request, 'tvdbid');
195
                $this->verifyEmptyParameter($request, 'traktid');
196
                $this->verifyEmptyParameter($request, 'rid');
197
                $this->verifyEmptyParameter($request, 'tvmazeid');
198
                $this->verifyEmptyParameter($request, 'imdbid');
199
                $this->verifyEmptyParameter($request, 'tmdbid');
200
                $this->verifyEmptyParameter($request, 'season');
201
                $this->verifyEmptyParameter($request, 'ep');
202
                $maxAge = $this->maxAge($request);
203
                UserRequest::addApiRequest($apiKey, $request->getRequestUri());
204
205
                $siteIdArr = [
206
                    'id' => $request->input('vid') ?? '0',
207
                    'tvdb' => $request->input('tvdbid') ?? '0',
208
                    'trakt' => $request->input('traktid') ?? '0',
209
                    'tvrage' => $request->input('rid') ?? '0',
210
                    'tvmaze' => $request->input('tvmazeid') ?? '0',
211
                    'imdb' => Str::replace('tt', '', $request->input('imdbid')) ?? '0',
212
                    'tmdb' => $request->input('tmdbid') ?? '0',
213
                ];
214
215
                // Process season only queries or Season and Episode/Airdate queries
216
217
                $series = $request->input('season') ?? '';
218
                $episode = $request->input('ep') ?? '';
219
220
                if (preg_match('#^(19|20)\d{2}$#', $series, $year) && str_contains($episode, '/')) {
221
                    $airDate = str_replace('/', '-', $year[0].'-'.$episode);
222
                }
223
224
                $relData = $releases->tvSearch(
225
                    $siteIdArr,
226
                    $series,
227
                    $episode,
228
                    $airDate ?? '',
229
                    $this->offset($request),
230
                    $this->limit($request),
231
                    $request->input('q') ?? '',
232
                    $this->categoryID($request),
233
                    $maxAge,
234
                    $minSize,
235
                    $catExclusions
236
                );
237
238
                $this->output($relData, $params, $outputXML, $offset, 'api');
239
                break;
240
241
                // Search movie releases.
242
            case 'm':
243
                $this->verifyEmptyParameter($request, 'q');
244
                $this->verifyEmptyParameter($request, 'imdbid');
245
                $maxAge = $this->maxAge($request);
246
                UserRequest::addApiRequest($apiKey, $request->getRequestUri());
247
248
                $imdbId = $request->has('imdbid') && $request->filled('imdbid') ? (int) $request->input('imdbid') : -1;
249
                $tmdbId = $request->has('tmdbid') && $request->filled('tmdbid') ? (int) $request->input('tmdbid') : -1;
250
                $traktId = $request->has('traktid') && $request->filled('traktid') ? (int) $request->input('traktid') : -1;
251
252
                $relData = $releases->moviesSearch(
253
                    $imdbId,
254
                    $tmdbId,
255
                    $traktId,
256
                    $this->offset($request),
257
                    $this->limit($request),
258
                    $request->input('q') ?? '',
259
                    $this->categoryID($request),
260
                    $maxAge,
261
                    $minSize,
262
                    $catExclusions
263
                );
264
265
                $this->addCoverURL(
266
                    $relData,
267
                    function ($release) {
268
                        return Utility::getCoverURL(['type' => 'movies', 'id' => $release->imdbid]);
269
                    }
270
                );
271
272
                $this->output($relData, $params, $outputXML, $offset, 'api');
273
                break;
274
275
                // Get NZB.
276
            case 'g':
277
                $this->verifyEmptyParameter($request, 'g');
278
                UserRequest::addApiRequest($apiKey, $request->getRequestUri());
279
                $relData = Release::checkGuidForApi($request->input('id'));
280
                if ($relData) {
281
                    return redirect(url('/getnzb?r='.$apiKey.'&id='.$request->input('id').(($request->has('del') && $request->input('del') === '1') ? '&del=1' : '')));
282
                }
283
284
                return Utility::showApiError(300, 'No such item (the guid you provided has no release in our database)');
285
286
                // Get individual NZB details.
287
            case 'd':
288
                if ($request->missing('id')) {
289
                    return Utility::showApiError(200, 'Missing parameter (guid is required for single release details)');
290
                }
291
292
                UserRequest::addApiRequest($apiKey, $request->getRequestUri());
293
                $data = Release::getByGuid($request->input('id'));
294
295
                $this->output($data, $params, $outputXML, $offset, 'api');
296
                break;
297
298
                // Get an NFO file for an individual release.
299
            case 'n':
300
                if ($request->missing('id')) {
301
                    return Utility::showApiError(200, 'Missing parameter (id is required for retrieving an NFO)');
302
                }
303
304
                UserRequest::addApiRequest($apiKey, $request->getRequestUri());
305
                $rel = Release::query()->where('guid', $request->input('id'))->first(['id', 'searchname']);
306
307
                if ($rel && $rel->isNotEmpty()) {
308
                    $data = ReleaseNfo::getReleaseNfo($rel->id);
309
                    if (! empty($data)) {
310
                        if ($request->has('o') && $request->input('o') === 'file') {
311
                            return response()->streamDownload(function () use ($data) {
312
                                echo $data['nfo'];
313
                            }, $rel['searchname'].'.nfo', ['Content-type:' => 'application/octet-stream']);
314
                        }
315
316
                        echo nl2br(Utility::cp437toUTF($data['nfo']));
317
                    } else {
318
                        return Utility::showApiError(300, 'Release does not have an NFO file associated.');
319
                    }
320
                } else {
321
                    return Utility::showApiError(300, 'Release does not exist.');
322
                }
323
                break;
324
                //
325
                // nzb add request
326
                // curl -X POST -F "file=@./The.File.nzb" "https://www.tabula-rasa.pw/api/V1/api?t=nzbadd&apikey=xxx"
327
                //
328
            case 'nzbAdd':
329
                if (! User::canPost($uid)) {
330
                    return response('User does not have permission to post', 403);
331
                }
332
333
                if ($request->missing('file')) {
334
                    return response('Missing parameter (file is required for adding an NZB)', 400);
335
                }
336
                if ($request->missing('apikey')) {
337
                    return response('Missing parameter (apikey is required for adding an NZB)', 400);
338
                }
339
340
                if (! $request->hasFile('file')) {
341
                    return response('Missing parameter (file is required for adding an NZB)', 400);
342
                }
343
344
                UserRequest::addApiRequest($apiKey, $request->getRequestUri());
345
346
                $nzbFile = $request->file('file');
347
348
                // Save the file to the server, get the name without the extension.
349
                if (File::isFile($nzbFile)) {
350
                    // We need to check if file is an actual nzb file.
351
                    if ($nzbFile->getClientOriginalExtension() !== 'nzb') {
352
                        return response('File is not an NZB file', 400);
353
                    }
354
                    // Check if the file is proper xml nzb file.
355
                    if (! Utility::isValidNewznabNzb($nzbFile->getContent())) {
0 ignored issues
show
Bug introduced by
The method isValidNewznabNzb() does not exist on Blacklight\utility\Utility. ( Ignorable by Annotation )

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

355
                    if (! Utility::/** @scrutinizer ignore-call */ isValidNewznabNzb($nzbFile->getContent())) {

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...
356
                        return response('File is not a valid Newznab NZB file', 400);
357
                    }
358
                    if (! File::isDirectory(config('nntmux.nzb_upload_folder'))) {
359
                        @File::makeDirectory(config('nntmux.nzb_upload_folder'), 0775, true);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for makeDirectory(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

359
                        /** @scrutinizer ignore-unhandled */ @File::makeDirectory(config('nntmux.nzb_upload_folder'), 0775, true);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
360
                    }
361
362
                    if (File::put(config('nntmux.nzb_upload_folder').$nzbFile->getClientOriginalName(), $nzbFile->getContent())) {
363
                        Log::channel('nzb_upload')->info('NZB file uploaded by API: '.$nzbFile->getClientOriginalName());
364
365
                        return response('NZB file uploaded successfully', 200);
366
                    }
367
368
                    Log::channel('nzb_upload')->warning('NZB file uploaded by API failed: '.$nzbFile->getClientOriginalName());
369
                } else {
370
                    Log::channel('nzb_upload')->warning('NZB file uploaded by API failed: '.$nzbFile->getClientOriginalName());
371
372
                    return response('NZB file upload failed', 500);
373
                }
374
375
                break;
376
377
                // Capabilities request.
378
            case 'c':
379
                $this->output([], $params, $outputXML, $offset, 'caps');
380
                break;
381
        }
382
    }
383
384
    /**
385
     * @throws \Exception
386
     */
387
    public function output($data, array $params, bool $xml, int $offset, string $type = '')
388
    {
389
        $this->type = $type;
390
        $options = [
391
            'Parameters' => $params,
392
            'Data' => $data,
393
            'Server' => $this->getForMenu(),
394
            'Offset' => $offset,
395
            'Type' => $type,
396
        ];
397
398
        // Generate the XML Response
399
        $response = (new XML_Response($options))->returnXML();
400
401
        if ($xml) {
402
            header('Content-type: text/xml');
403
        } else {
404
            // JSON encode the XMLWriter response
405
            $response = json_encode(xml_to_array($response), JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT + JSON_UNESCAPED_SLASHES);
0 ignored issues
show
Bug introduced by
It seems like $response can also be of type false; however, parameter $xml of xml_to_array() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

405
            $response = json_encode(xml_to_array(/** @scrutinizer ignore-type */ $response), JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT + JSON_UNESCAPED_SLASHES);
Loading history...
406
            header('Content-type: application/json');
407
        }
408
        if ($response === false) {
409
            return Utility::showApiError(201);
410
        } else {
411
            header('Content-Length: '.\strlen($response));
412
            echo $response;
413
            exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
414
        }
415
    }
416
417
    /**
418
     * Collect and return various capability information for usage in API.
419
     *
420
     *
421
     * @throws \Exception
422
     */
423
    public function getForMenu(): array
424
    {
425
        $serverroot = url('/');
426
427
        return [
428
            'server' => [
429
                'title' => config('app.name'),
430
                'strapline' => Settings::settingValue('strapline'),
431
                'email' => config('mail.from.address'),
432
                'meta' => Settings::settingValue('metakeywords'),
433
                'url' => $serverroot,
434
                'image' => $serverroot.'/assets/images/tmux_logo.png',
435
            ],
436
            'limits' => [
437
                'max' => 100,
438
                'default' => 100,
439
            ],
440
            'registration' => [
441
                'available' => 'yes',
442
                'open' => (int) Settings::settingValue('registerstatus') === 0 ? 'yes' : 'no',
443
            ],
444
            'searching' => [
445
                'search' => ['available' => 'yes', 'supportedParams' => 'q'],
446
                'tv-search' => ['available' => 'yes', 'supportedParams' => 'q,vid,tvdbid,traktid,rid,tvmazeid,imdbid,tmdbid,season,ep'],
447
                'movie-search' => ['available' => 'yes', 'supportedParams' => 'q,imdbid, tmdbid, traktid'],
448
                'audio-search' => ['available' => 'no',  'supportedParams' => ''],
449
            ],
450
            'categories' => $this->type === 'caps'
451
                ? Category::getForMenu()
452
                : null,
453
        ];
454
    }
455
456
    /**
457
     * @return Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Foundation\Application|\Illuminate\Http\Response|int
458
     */
459
    public function maxAge(Request $request)
460
    {
461
        $maxAge = -1;
462
        if ($request->has('maxage')) {
463
            if (! $request->filled('maxage')) {
464
                return Utility::showApiError(201, 'Incorrect parameter (maxage must not be empty)');
465
            } elseif (! is_numeric($request->input('maxage'))) {
466
                return Utility::showApiError(201, 'Incorrect parameter (maxage must be numeric)');
467
            } else {
468
                $maxAge = (int) $request->input('maxage');
469
            }
470
        }
471
472
        return $maxAge;
473
    }
474
475
    /**
476
     * Verify cat parameter.
477
     */
478
    public function categoryID(Request $request): array
479
    {
480
        $categoryID[] = -1;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$categoryID was never initialized. Although not strictly required by PHP, it is generally a good practice to add $categoryID = array(); before regardless.
Loading history...
481
        if ($request->has('cat')) {
482
            $categoryIDs = urldecode($request->input('cat'));
483
            // Append Web-DL category ID if HD present for SickBeard / Sonarr compatibility.
484
            if (str_contains($categoryIDs, (string) Category::TV_HD) && ! str_contains($categoryIDs, (string) Category::TV_WEBDL) && (int) Settings::settingValue('catwebdl') === 0) {
485
                $categoryIDs .= (','.Category::TV_WEBDL);
486
            }
487
            $categoryID = explode(',', $categoryIDs);
488
        }
489
490
        return $categoryID;
491
    }
492
493
    /**
494
     * Verify groupName parameter.
495
     *
496
     *
497
     * @throws \Exception
498
     */
499
    public function group(Request $request): string|int|bool
500
    {
501
        $groupName = -1;
502
        if ($request->has('group')) {
503
            $group = UsenetGroup::isValidGroup($request->input('group'));
504
            if ($group !== false) {
505
                $groupName = $group;
506
            }
507
        }
508
509
        return $groupName;
510
    }
511
512
    /**
513
     * Verify limit parameter.
514
     */
515
    public function limit(Request $request): int
516
    {
517
        $limit = 100;
518
        if ($request->has('limit') && is_numeric($request->input('limit'))) {
519
            $limit = (int) $request->input('limit');
520
        }
521
522
        return $limit;
523
    }
524
525
    /**
526
     * Verify offset parameter.
527
     */
528
    public function offset(Request $request): int
529
    {
530
        $offset = 0;
531
        if ($request->has('offset') && is_numeric($request->input('offset'))) {
532
            $offset = (int) $request->input('offset');
533
        }
534
535
        return $offset;
536
    }
537
538
    /**
539
     * Check if a parameter is empty.
540
     */
541
    public function verifyEmptyParameter(Request $request, string $parameter)
542
    {
543
        if ($request->has($parameter) && $request->isNotFilled($parameter)) {
544
            return Utility::showApiError(201, 'Incorrect parameter ('.$parameter.' must not be empty)');
545
        }
546
    }
547
548
    public function addCoverURL(&$releases, callable $getCoverURL): void
549
    {
550
        if ($releases && \count($releases)) {
551
            foreach ($releases as $key => $release) {
552
                if (isset($release->id)) {
553
                    $release->coverurl = $getCoverURL($release);
554
                }
555
            }
556
        }
557
    }
558
}
559