Passed
Push — master ( 295b00...fd1954 )
by Kylian
01:34
created

BeatSaverAPI::searchMap()   F

Complexity

Conditions 18
Paths > 20000

Size

Total Lines 29
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 18
eloc 23
c 4
b 0
f 0
nc 131072
nop 20
dl 0
loc 29
rs 0.7

How to fix   Complexity    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
namespace KriKrixs;
4
5
use KriKrixs\object\beatmap\BeatMap;
6
use KriKrixs\object\response\ResponseDownload;
7
use KriKrixs\object\response\ResponseMap;
8
use KriKrixs\object\response\ResponseMaps;
9
use KriKrixs\object\response\ResponseUser;
10
use KriKrixs\object\user\User;
11
12
class BeatSaverAPI
13
{
14
    const BEATSAVER_URL     = "https://api.beatsaver.com/";
15
    const BEATSAVER_CDN_URL = "https://cdn.beatsaver.com/";
16
    const MAX_HASHES_NUMBER = 50;
17
    const MAX_CALL_PER_SECS = 10;
18
19
    private string $userAgent;
20
21
    /**
22
     * BeatSaverAPI constructor
23
     * @param string $userAgent User Agent to provide to Beat Saver API
24
     * @param bool $needAutoloader If you don't use it with composer = true
25
     */
26
    public function __construct(string $userAgent, bool $needAutoloader = false)
27
    {
28
        if($needAutoloader)
29
            $this->autoload("./");
30
31
        $this->userAgent = $userAgent;
32
    }
33
34
    private function autoload($directory) {
35
        if(is_dir($directory)) {
36
            $scan = scandir($directory);
37
            unset($scan[0], $scan[1]); //unset . and ..
38
            foreach($scan as $file) {
39
                if(is_dir($directory."/".$file)) {
40
                    $this->autoload($directory."/".$file);
41
                } else {
42
                    if(strpos($file, '.php') !== false) {
43
                        include_once($directory."/".$file);
44
                    }
45
                }
46
            }
47
        }
48
    }
49
50
    /**
51
     * Private calling API function
52
     * @param string $endpoint
53
     * @return string|null
54
     */
55
    private function callAPI(string $endpoint): ?string
56
    {
57
        $curl = curl_init();
58
59
        curl_setopt_array($curl, [
60
            CURLOPT_URL => self::BEATSAVER_URL . $endpoint,
61
            CURLOPT_USERAGENT => $this->userAgent,
62
            CURLOPT_RETURNTRANSFER => true
63
        ]);
64
65
        $result = curl_exec($curl);
66
67
        curl_close($curl);
68
69
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result could return the type true which is incompatible with the type-hinted return null|string. Consider adding an additional type-check to rule them out.
Loading history...
70
    }
71
72
    ////////////
73
    /// CALL ///
74
    ///////////
75
76
    /**
77
     * Private building response functions
78
     * @param string $endpoint
79
     * @return ResponseMap
80
     */
81
    private function getMap(string $endpoint): ResponseMap
82
    {
83
        $response = new ResponseMap();
84
85
        $apiResult = $this->callAPI($endpoint);
86
87
        if($apiResult === false || $apiResult == "Not Found") {
88
            $response->setErrorStatus(true)->setErrorMessage("[getMap] Something went wrong with the API call.");
89
            return $response;
90
        }
91
92
        $response->setBeatMap(new BeatMap(json_decode($apiResult)));
93
94
        return $response;
95
    }
96
97
    ///////////////
98
    /// Get Map ///
99
    ///////////////
100
101
    /**
102
     * Get map by ID (Same as BSR Key)
103
     * @param string $id Map ID
104
     * @return ResponseMap
105
     */
106
    public function getMapByID(string $id): ResponseMap
107
    {
108
        return $this->getMap("/maps/id/" . $id);
109
    }
110
111
    /**
112
     * Get map by BSR Key (Same as ID)
113
     * @param string $bsrKey Map BSR key
114
     * @return ResponseMap
115
     */
116
    public function getMapByKey(string $bsrKey): ResponseMap
117
    {
118
        return $this->getMap("/maps/id/" . $bsrKey);
119
    }
120
121
    /**
122
     * Get map by Hash
123
     * @param string $hash Hash of the map
124
     * @return ResponseMap
125
     */
126
    public function getMapByHash(string $hash): ResponseMap
127
    {
128
        return $this->getMap("/maps/hash/" . $hash);
129
    }
130
131
    ////////////////
132
    /// Get Maps ///
133
    ////////////////
134
135
    /**
136
     * Private building response functions
137
     * @param string $endpoint
138
     * @return ResponseMaps
139
     */
140
    private function getMaps(string $endpoint): ResponseMaps
141
    {
142
        $response = new ResponseMaps();
143
144
        $apiResult = $this->callAPI($endpoint);
145
146
        if($apiResult === false || $apiResult == "Not Found") {
147
            $response->setErrorStatus(true)->setErrorMessage("[getMap] Something went wrong with the API call.");
148
            return $response;
149
        }
150
151
        $response->setRawBeatMaps(json_decode($apiResult));
152
153
        return $response;
154
    }
155
156
    /**
157
     * Get maps by IDs (Same as BSR keys)
158
     * @param array $ids Array of maps ID (Same as BSR keys)
159
     * @return array Array of BeatMap object
160
     * @deprecated This function may end up with a 429 - Too many request. Use getMapsByHashes instead to avoid it.
161
     */
162
//    public function getMapsByIds(array $ids): array
163
//    {
164
//        return $this->multiQuery->DoMultiQuery($ids, false);
165
//    }
166
167
    /**
168
     * Get maps by BSR Keys (Same as IDs)
169
     * @param array $keys Array of maps BSR key (Same as IDs)
170
     * @return array Array of BeatMap object
171
     * @deprecated This function may end up with a 429 - Too many request. Use getMapsByHashes instead to avoid it.
172
     */
173
//    public function getMapsByKeys(array $keys): array
174
//    {
175
//        return $this->multiQuery->DoMultiQuery($keys, false);
176
//    }
177
178
    /**
179
     * Get maps by hashes
180
     * @param array $hashes Array of maps hash (minimum 2 hash)
181
     * @return ResponseMaps Array of BeatMap object
182
     */
183
    public function getMapsByHashes(array $hashes): ResponseMaps
184
    {
185
        $endpoint = "/maps/hash/";
186
        $hashesString = $endpoint;
187
        $mapsArray = [];
188
        $i = 0;
189
        $callNumber = 0;
190
        $result = new ResponseMaps();
191
192
        if(count($hashes) < 2) {
193
            return $result->setErrorStatus(true)->setErrorMessage("This functions require a minimum of 2 hashes in the array");
194
        }
195
196
        foreach($hashes as $hash) {
197
            $hashesString .= $hash;
198
199
            if($i !== 0 && $i % self::MAX_HASHES_NUMBER === 0) {
200
                if($callNumber === self::MAX_CALL_PER_SECS) {
201
                    sleep(1);
202
                    $callNumber = 0;
203
                }
204
205
                $maps = $this->getMaps($hashesString);
206
                $callNumber++;
207
208
                $mapsArray = array_merge($mapsArray, $maps->getBeatMaps());
209
210
                if(!isset($mapsArray["errorStatus"]) || !$mapsArray["errorStatus"]) {
211
                    $mapsArray["errorStatus"] = $maps->getErrorStatus();
212
                    $mapsArray["errorMessage"] = $maps->getErrorMessage();
213
                }
214
215
                $hashesString = $endpoint;
216
            } else {
217
                $hashesString .= ",";
218
            }
219
220
            $i++;
221
        }
222
223
224
        if($i !== 0) {
225
            $maps = $this->getMaps($hashesString);
226
            $mapsArray = array_merge($mapsArray, $maps->getBeatMaps());
227
228
            if(!isset($mapsArray["errorStatus"]) || !$mapsArray["errorStatus"]) {
229
                $mapsArray["errorStatus"] = $maps->getErrorStatus();
230
                $mapsArray["errorMessage"] = $maps->getErrorMessage();
231
            }
232
        }
233
234
        if(isset($mapsArray["errorStatus"]) && $mapsArray["errorStatus"])
235
            $result->setErrorStatus( $mapsArray["errorStatus"])->setErrorMessage( $mapsArray["errorMessage"]);
236
237
        unset($mapsArray["errorStatus"]);
238
        unset($mapsArray["errorMessage"]);
239
240
        return $result->setBeatMaps($mapsArray);
241
    }
242
243
    /**
244
     * Private building response functions
245
     * @param string $endpoint
246
     * @param int $numberOfPage
247
     * @param int $startPage
248
     * @return ResponseMaps
249
     */
250
    private function getMapsByEndpoint(string $endpoint, int $numberOfPage = 0, int $startPage = 0): ResponseMaps
251
    {
252
        $response = new ResponseMaps();
253
        $maps = [];
254
        $callNumber = 0;
255
256
        // Latest
257
        if($numberOfPage === 0 && $startPage === 0){
258
            $apiResult = json_decode($this->callAPI(str_ireplace("page", 0, $endpoint)));
0 ignored issues
show
Bug introduced by
It seems like str_ireplace('page', 0, $endpoint) can also be of type array; however, parameter $endpoint of KriKrixs\BeatSaverAPI::callAPI() 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

258
            $apiResult = json_decode($this->callAPI(/** @scrutinizer ignore-type */ str_ireplace("page", 0, $endpoint)));
Loading history...
259
260
            if($apiResult === false || $apiResult == "Not Found") {
261
                $response->setErrorStatus(true)->setErrorMessage("[getMaps] Something went wrong with the API call while calling the first page.");
262
                return $response;
263
            } else{
264
                foreach ($apiResult->docs as $beatmap) {
265
                    $maps[] = new BeatMap($beatmap);
266
                }
267
            }
268
        } else {
269
            for($i = $startPage; $i < ($i + $numberOfPage); $i++){
270
                if($callNumber === self::MAX_CALL_PER_SECS) {
271
                    sleep(1);
272
                    $callNumber = 0;
273
                }
274
275
                $apiResult = json_decode($this->callAPI(str_ireplace("page", $i, $endpoint)));
276
                $callNumber++;
277
278
                if($apiResult === false || $apiResult == "Not Found") {
279
                    $response->setErrorStatus(true)->setErrorMessage("[getMaps] Something went wrong with the API call while calling page number " . $i . ".");
280
281
                    if($apiResult == "Not Found")
282
                        return $response;
283
                }
284
285
                foreach ($apiResult->docs as $beatmap) {
286
                    $maps[] = new BeatMap($beatmap);
287
                }
288
            }
289
        }
290
291
        $response->setBeatMaps($maps);
292
293
        return $response;
294
    }
295
296
    /**
297
     * Get maps by Uploader ID! Not the uploader name!
298
     * @param int $uploaderID Uploader ID on BeatSaver
299
     * @param int $numberOfPage The number of page you want to be returned
300
     * @param int $startPage The starting page
301
     * @return ResponseMaps
302
     */
303
    public function getMapsByUploaderID(int $uploaderID, int $numberOfPage = 0, int $startPage = 0): ResponseMaps
304
    {
305
        return $this->getMapsByEndpoint("/maps/uploader/" . $uploaderID . "/page", $numberOfPage, $startPage);
306
    }
307
308
    /**
309
     * Get 20 latest maps
310
     * @param bool $autoMapper Do you want automapper or not ?
311
     * @return ResponseMaps
312
     */
313
    public function getMapsSortedByLatest(bool $autoMapper): ResponseMaps
314
    {
315
        return $this->getMapsByEndpoint("/maps/latest?automapper=" . var_export($autoMapper, true));
316
    }
317
318
    /**
319
     * Get maps sorted by plays numbers
320
     * @param int $numberOfPage The number of page you want to be returned
321
     * @param int $startPage The starting page
322
     * @return ResponseMaps
323
     */
324
    public function getMapsSortedByPlays(int $numberOfPage = 0, int $startPage = 0): ResponseMaps
325
    {
326
        return $this->getMapsByEndpoint("/maps/plays/page", $numberOfPage, $startPage);
327
    }
328
329
    /**
330
     * Search a map (Set null to a parameter to not use it)
331
     * @param int $startPage Start page number
332
     * @param int $numberOfPage Number of page wanted
333
     * @param int $sortOrder (Default 1) 1 = Latest | 2 = Relevance | 3 = Rating
334
     * @param string|null $mapName (Optional) Map name
335
     * @param \DateTime|null $startDate (Optional) Map made from this date
336
     * @param \DateTime|null $endDate (Optional) Map made to this date
337
     * @param bool $ranked (Optional) Want ranked or not ?
338
     * @param bool $automapper (Optional) Want automapper or not ?
339
     * @param bool $chroma (Optional) Want chroma or not ?
340
     * @param bool $noodle (Optional) Want noodle or not ?
341
     * @param bool $cinema (Optional) Want cinema or not ?
342
     * @param bool $fullSpread (Optional) Want fullSpread or not ?
343
     * @param float|null $minBpm (Optional) Minimum BPM
344
     * @param float|null $maxBpm (Optional) Maximum BPM
345
     * @param float|null $minNps (Optional) Minimum NPS
346
     * @param float|null $maxNps (Optional) Maximum NPS
347
     * @param float|null $minRating (Optional) Minimum Rating
348
     * @param float|null $maxRating (Optional) Maximum Rating
349
     * @param int|null $minDuration (Optional) Minimum Duration
350
     * @param int|null $maxDuration (Optional) Maximum Duration
351
     * @return ResponseMaps
352
     */
353
    public function searchMap(int $startPage = 0, int $numberOfPage = 1, int $sortOrder = 1, string $mapName = null, \DateTime $startDate = null, \DateTime $endDate = null, bool $ranked = false, bool $automapper = false, bool $chroma = false, bool $noodle = false, bool $cinema = false, bool $fullSpread = false, float $minBpm = null, float $maxBpm = null, float $minNps = null, float $maxNps = null, float $minRating = null, float $maxRating = null, int $minDuration = null, int $maxDuration = null): ResponseMaps
354
    {
355
        $sort = [
356
            1 => "Latest",
357
            2 => "Relevance",
358
            3 => "Rating"
359
        ];
360
361
        $endpoint = "/search/text/page?sortOrder=" . $sort[$sortOrder];
362
363
        if($mapName)                $endpoint .= "&q=" . urlencode($mapName);
364
        if($startDate)              $endpoint .= "&from=" . $startDate->format("Y-m-d");
365
        if($endDate)                $endpoint .= "&to=" . $endDate->format("Y-m-d");
366
        if($ranked)                 $endpoint .= "&ranked=" . /** @scrutinizer ignore-type */ var_export($ranked, true);
367
        if($automapper)             $endpoint .= "&automapper=" . /** @scrutinizer ignore-type */ var_export($automapper, true);
368
        if($chroma)                 $endpoint .= "&chroma=" . /** @scrutinizer ignore-type */ var_export($chroma, true);
369
        if($noodle)                 $endpoint .= "&noodle=" . /** @scrutinizer ignore-type */ var_export($noodle, true);
370
        if($cinema)                 $endpoint .= "&cinema=" . /** @scrutinizer ignore-type */ var_export($cinema, true);
371
        if($fullSpread)             $endpoint .= "&fullSpread=" . /** @scrutinizer ignore-type */ var_export($fullSpread, true);
372
        if($minBpm)                 $endpoint .= "&minBpm=" . /** @scrutinizer ignore-type */ $minBpm;
373
        if($maxBpm)                 $endpoint .= "&maxBpm=" . /** @scrutinizer ignore-type */ $maxBpm;
374
        if($minNps)                 $endpoint .= "&minNps=" . /** @scrutinizer ignore-type */ $minNps;
375
        if($maxNps)                 $endpoint .= "&maxNps=" . /** @scrutinizer ignore-type */ $maxNps;
376
        if($minRating)              $endpoint .= "&minRating=" . /** @scrutinizer ignore-type */ $minRating;
377
        if($maxRating)              $endpoint .= "&maxRating=" . /** @scrutinizer ignore-type */ $maxRating;
378
        if($minDuration !== null)   $endpoint .= "&minDuration=" . /** @scrutinizer ignore-type */ $minDuration;
379
        if($maxDuration !== null)   $endpoint .= "&maxDuration=" . /** @scrutinizer ignore-type */ $maxDuration;
380
381
        return $this->getMapsByEndpoint($endpoint, $numberOfPage, $startPage);
382
    }
383
384
    ////////////////
385
    /// Get User ///
386
    ////////////////
387
388
    /**
389
     * Private building response functions
390
     * @param string $endpoint
391
     * @return ResponseUser
392
     */
393
    private function getUser(string $endpoint): ResponseUser
394
    {
395
        $response = new ResponseUser();
396
397
        $apiResult = $this->callAPI($endpoint);
398
399
        if($apiResult === false || $apiResult == "Not Found") {
400
            $response->setErrorStatus(true)->setErrorMessage("[getMap] Something went wrong with the API call.");
401
            return $response;
402
        }
403
404
        $response->setUser(new User(json_decode($apiResult)));
405
406
        return $response;
407
    }
408
409
    /**
410
     * Get user's infos by UserID
411
     * @param int $id User ID
412
     * @return ResponseUser
413
     */
414
    public function getUserByID(int $id): ResponseUser
415
    {
416
        return $this->getUser("/users/id/" . $id);
417
    }
418
419
    ////////////////////
420
    /// Download map ///
421
    ////////////////////
422
423
    private function downloadMapZipAndCover(array $hashes, string $targetDir): ResponseDownload
424
    {
425
        $response = new ResponseDownload();
426
427
        if (!file_exists($targetDir)) {
428
            mkdir($targetDir, 0777, true);
429
        }
430
431
        if(substr($targetDir, -1) !== "/")
432
            $targetDir .= "/";
433
434
        foreach ($hashes as $hash) {
435
            //The path & filename to save to.
436
            $saveTo = $targetDir . $hash;
437
438
            echo $hash . ": ";
439
440
            $error = false;
441
442
            echo "map" . ": ";
443
444
            $ch = curl_init(self::BEATSAVER_CDN_URL . $hash . ".zip");
445
            curl_setopt($ch, CURLOPT_USERAGENT, $this->userAgent);
446
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
447
            curl_setopt($ch, CURLOPT_ENCODING, "");
448
            curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET");
449
450
            $result = curl_exec($ch);
451
452
            if(curl_errno($ch) === 0) {
453
                $response->pushDownloadMapHash($hash);
454
                echo "Ok ";
455
            } else {
456
                $response->pushFailMapHash($hash)->setErrorStatus(true)->setErrorMessage("Something went wrong with some maps");
457
                $error = true;
458
                echo "Error ";
459
            }
460
461
            file_put_contents($saveTo . ".zip", $result);
462
463
            curl_close($ch);
464
465
            echo "cover" . ": ";
466
467
            $ch = curl_init(self::BEATSAVER_CDN_URL . $hash . ".jpg");
468
            curl_setopt($ch, CURLOPT_USERAGENT, $this->userAgent);
469
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
470
            curl_setopt($ch, CURLOPT_ENCODING, "");
471
            curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET");
472
473
            $result = curl_exec($ch);
474
475
            if(curl_errno($ch) === 0) {
476
                $response->pushDownloadMapHash($hash);
477
                echo "Ok ";
478
            } else {
479
                $response->pushFailMapHash($hash)->setErrorStatus(true)->setErrorMessage("Something went wrong with some maps");
480
                $error = true;
481
                echo "Error ";
482
            }
483
484
            file_put_contents($saveTo . ".jpg", $result);
485
486
            curl_close($ch);
487
488
            echo "save: " . ($error ? "No" : "Yes") . "\n";
489
        }
490
491
        return $response;
492
    }
493
494
    /**
495
     * Download maps using id (Same as BSR Key)
496
     * @param array $ids Array of maps IDs (Same as BSR Key)
497
     * @param string $targetDir Path to download dir
498
     * @deprecated Will not work, use downloadMapByHashes instead
499
     * @return ResponseDownload
500
     */
501
    public function downloadMapByIds(array $ids, string $targetDir): ResponseDownload
502
    {
503
        return $this->downloadMapZipAndCover($ids, $targetDir);
504
    }
505
506
    /**
507
     * Download maps using bsr key (Same as ID)
508
     * @param array $keys Array of maps keys (Same as ID)
509
     * @param string $targetDir Path to download dir
510
     * @deprecated Will not work, use downloadMapByHashes instead
511
     * @return ResponseDownload
512
     */
513
    public function downloadMapByKeys(array $keys, string $targetDir): ResponseDownload
514
    {
515
        return $this->downloadMapZipAndCover($keys, $targetDir);
516
    }
517
518
    /**
519
     * Download maps using hashes
520
     * @param array $hashes Array of maps hashes
521
     * @param string $targetDir Path to download dir
522
     * @return ResponseDownload
523
     */
524
    public function downloadMapByHashes(array $hashes, string $targetDir): ResponseDownload
525
    {
526
        return $this->downloadMapZipAndCover($hashes, $targetDir);
527
    }
528
}