BeatSaverAPI::searchMap()   F
last analyzed

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) {
0 ignored issues
show
introduced by
The condition $apiResult === false is always false.
Loading history...
88
            $response->setErrorStatus(true)->setErrorMessage("[getMap] Something went wrong with the API call (" . $endpoint . ")");
89
            return $response;
90
        } elseif((json_decode($apiResult))->error == "Not Found") {
91
            $response->setErrorStatus(true)->setErrorMessage("[getMap] Map not found (" . $endpoint . ")");
92
            return $response;
93
        }
94
95
        $response->setBeatMap(new BeatMap(json_decode($apiResult)));
96
97
        return $response;
98
    }
99
100
    ///////////////
101
    /// Get Map ///
102
    ///////////////
103
104
    /**
105
     * Get map by ID (Same as BSR Key)
106
     * @param string $id Map ID
107
     * @return ResponseMap
108
     */
109
    public function getMapByID(string $id): ResponseMap
110
    {
111
        return $this->getMap("/maps/id/" . $id);
112
    }
113
114
    /**
115
     * Get map by BSR Key (Same as ID)
116
     * @param string $bsrKey Map BSR key
117
     * @return ResponseMap
118
     */
119
    public function getMapByKey(string $bsrKey): ResponseMap
120
    {
121
        return $this->getMap("/maps/id/" . $bsrKey);
122
    }
123
124
    /**
125
     * Get map by Hash
126
     * @param string $hash Hash of the map
127
     * @return ResponseMap
128
     */
129
    public function getMapByHash(string $hash): ResponseMap
130
    {
131
        return $this->getMap("/maps/hash/" . $hash);
132
    }
133
134
    ////////////////
135
    /// Get Maps ///
136
    ////////////////
137
138
    /**
139
     * Private building response functions
140
     * @param string $endpoint
141
     * @return ResponseMaps
142
     */
143
    private function getMaps(string $endpoint): ResponseMaps
144
    {
145
        $response = new ResponseMaps();
146
147
        $apiResult = $this->callAPI($endpoint);
148
149
        if($apiResult === false || $apiResult == "Not Found") {
150
            $response->setErrorStatus(true)->setErrorMessage("[getMap] Something went wrong with the API call.");
151
            return $response;
152
        }
153
154
        $response->setRawBeatMaps(json_decode($apiResult));
155
156
        return $response;
157
    }
158
159
    /**
160
     * Get maps by IDs (Same as BSR keys)
161
     * @param array $ids Array of maps ID (Same as BSR keys)
162
     * @return array Array of BeatMap object
163
     * @deprecated This function may end up with a 429 - Too many request. Use getMapsByHashes instead to avoid it.
164
     */
165
//    public function getMapsByIds(array $ids): array
166
//    {
167
//        return $this->multiQuery->DoMultiQuery($ids, false);
168
//    }
169
170
    /**
171
     * Get maps by BSR Keys (Same as IDs)
172
     * @param array $keys Array of maps BSR key (Same as IDs)
173
     * @return array Array of BeatMap object
174
     * @deprecated This function may end up with a 429 - Too many request. Use getMapsByHashes instead to avoid it.
175
     */
176
//    public function getMapsByKeys(array $keys): array
177
//    {
178
//        return $this->multiQuery->DoMultiQuery($keys, false);
179
//    }
180
181
    /**
182
     * Get maps by hashes
183
     * @param array $hashes Array of maps hash (minimum 2 hash)
184
     * @return ResponseMaps Array of BeatMap object
185
     */
186
    public function getMapsByHashes(array $hashes): ResponseMaps
187
    {
188
        $endpoint = "/maps/hash/";
189
        $hashesString = $endpoint;
190
        $mapsArray = [];
191
        $i = 0;
192
        $callNumber = 0;
193
        $result = new ResponseMaps();
194
195
        if(count($hashes) < 2) {
196
            return $result->setErrorStatus(true)->setErrorMessage("This functions require a minimum of 2 hashes in the array");
197
        }
198
199
        foreach($hashes as $hash) {
200
            $hashesString .= $hash;
201
202
            if($i !== 0 && $i % self::MAX_HASHES_NUMBER === 0) {
203
                if($callNumber === self::MAX_CALL_PER_SECS) {
204
                    sleep(1);
205
                    $callNumber = 0;
206
                }
207
208
                $maps = $this->getMaps($hashesString);
209
                $callNumber++;
210
211
                $mapsArray = array_merge($mapsArray, $maps->getBeatMaps());
212
213
                if(!isset($mapsArray["errorStatus"]) || !$mapsArray["errorStatus"]) {
214
                    $mapsArray["errorStatus"] = $maps->getErrorStatus();
215
                    $mapsArray["errorMessage"] = $maps->getErrorMessage();
216
                }
217
218
                $hashesString = $endpoint;
219
            } else {
220
                $hashesString .= ",";
221
            }
222
223
            $i++;
224
        }
225
226
227
        if($i !== 0) {
228
            $maps = $this->getMaps(substr($hashesString, 0, -1));
229
            $mapsArray = array_merge($mapsArray, $maps->getBeatMaps());
230
231
            if(!isset($mapsArray["errorStatus"]) || !$mapsArray["errorStatus"]) {
232
                $mapsArray["errorStatus"] = $maps->getErrorStatus();
233
                $mapsArray["errorMessage"] = $maps->getErrorMessage();
234
            }
235
        }
236
237
        if(isset($mapsArray["errorStatus"]) && $mapsArray["errorStatus"])
238
            $result->setErrorStatus( $mapsArray["errorStatus"])->setErrorMessage( $mapsArray["errorMessage"]);
239
240
        unset($mapsArray["errorStatus"]);
241
        unset($mapsArray["errorMessage"]);
242
243
        return $result->setBeatMaps($mapsArray);
244
    }
245
246
    /**
247
     * Private building response functions
248
     * @param string $endpoint
249
     * @param int $numberOfPage
250
     * @param int $startPage
251
     * @return ResponseMaps
252
     */
253
    private function getMapsByEndpoint(string $endpoint, int $numberOfPage = 0, int $startPage = 0): ResponseMaps
254
    {
255
        $response = new ResponseMaps();
256
        $maps = [];
257
        $callNumber = 0;
258
259
        // Latest
260
        if($numberOfPage === 0 && $startPage === 0){
261
            $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

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