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

260
            $apiResult = json_decode($this->callAPI(/** @scrutinizer ignore-type */ str_ireplace("page", 0, $endpoint)));
Loading history...
261
262
            if($apiResult === false || $apiResult == "Not Found") {
263
                $response->setErrorStatus(true)->setErrorMessage("[getMaps] Something went wrong with the API call while calling the first page.");
264
                return $response;
265
            } else{
266
                foreach ($apiResult->docs as $beatmap) {
267
                    $maps[] = new BeatMap($beatmap);
268
                }
269
            }
270
        } else {
271
            for($i = $startPage; $i < ($i + $numberOfPage); $i++){
272
                if($callNumber === self::MAX_CALL_PER_SECS) {
273
                    sleep(1);
274
                    $callNumber = 0;
275
                }
276
277
                $apiResult = json_decode($this->callAPI(str_ireplace("page", $i, $endpoint)));
278
                $callNumber++;
279
280
                if($apiResult === false || $apiResult == "Not Found") {
281
                    $response->setErrorStatus(true)->setErrorMessage("[getMaps] Something went wrong with the API call while calling page number " . $i . ".");
282
283
                    if($apiResult == "Not Found")
284
                        return $response;
285
                }
286
287
                foreach ($apiResult->docs as $beatmap) {
288
                    $maps[] = new BeatMap($beatmap);
289
                }
290
            }
291
        }
292
293
        $response->setBeatMaps($maps);
294
295
        return $response;
296
    }
297
298
    /**
299
     * Get maps by Uploader ID! Not the uploader name!
300
     * @param int $uploaderID Uploader ID on BeatSaver
301
     * @param int $numberOfPage The number of page you want to be returned
302
     * @param int $startPage The starting page
303
     * @return ResponseMaps
304
     */
305
    public function getMapsByUploaderID(int $uploaderID, int $numberOfPage = 0, int $startPage = 0): ResponseMaps
306
    {
307
        return $this->getMapsByEndpoint("/maps/uploader/" . $uploaderID . "/page", $numberOfPage, $startPage);
308
    }
309
310
    /**
311
     * Get 20 latest maps
312
     * @param bool $autoMapper Do you want automapper or not ?
313
     * @return ResponseMaps
314
     */
315
    public function getMapsSortedByLatest(bool $autoMapper): ResponseMaps
316
    {
317
        return $this->getMapsByEndpoint("/maps/latest?automapper=" . var_export($autoMapper, true));
318
    }
319
320
    /**
321
     * Get maps sorted by plays numbers
322
     * @param int $numberOfPage The number of page you want to be returned
323
     * @param int $startPage The starting page
324
     * @return ResponseMaps
325
     */
326
    public function getMapsSortedByPlays(int $numberOfPage = 0, int $startPage = 0): ResponseMaps
327
    {
328
        return $this->getMapsByEndpoint("/maps/plays/page", $numberOfPage, $startPage);
329
    }
330
331
    /**
332
     * Search a map (Set null to a parameter to not use it)
333
     * @param int $startPage Start page number
334
     * @param int $numberOfPage Number of page wanted
335
     * @param int $sortOrder (Default 1) 1 = Latest | 2 = Relevance | 3 = Rating
336
     * @param string|null $mapName (Optional) Map name
337
     * @param \DateTime|null $startDate (Optional) Map made from this date
338
     * @param \DateTime|null $endDate (Optional) Map made to this date
339
     * @param bool $ranked (Optional) Want ranked or not ?
340
     * @param bool $automapper (Optional) Want automapper or not ?
341
     * @param bool $chroma (Optional) Want chroma or not ?
342
     * @param bool $noodle (Optional) Want noodle or not ?
343
     * @param bool $cinema (Optional) Want cinema or not ?
344
     * @param bool $fullSpread (Optional) Want fullSpread or not ?
345
     * @param float|null $minBpm (Optional) Minimum BPM
346
     * @param float|null $maxBpm (Optional) Maximum BPM
347
     * @param float|null $minNps (Optional) Minimum NPS
348
     * @param float|null $maxNps (Optional) Maximum NPS
349
     * @param float|null $minRating (Optional) Minimum Rating
350
     * @param float|null $maxRating (Optional) Maximum Rating
351
     * @param int|null $minDuration (Optional) Minimum Duration
352
     * @param int|null $maxDuration (Optional) Maximum Duration
353
     * @return ResponseMaps
354
     */
355
    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
356
    {
357
        $sort = [
358
            1 => "Latest",
359
            2 => "Relevance",
360
            3 => "Rating"
361
        ];
362
363
        $endpoint = "/search/text/page?sortOrder=" . $sort[$sortOrder];
364
365
        if($mapName)                $endpoint .= "&q=" . urlencode($mapName);
366
        if($startDate)              $endpoint .= "&from=" . $startDate->format("Y-m-d");
367
        if($endDate)                $endpoint .= "&to=" . $endDate->format("Y-m-d");
368
        if($ranked)                 $endpoint .= "&ranked=" . /** @scrutinizer ignore-type */ var_export($ranked, true);
369
        if($automapper)             $endpoint .= "&automapper=" . /** @scrutinizer ignore-type */ var_export($automapper, true);
370
        if($chroma)                 $endpoint .= "&chroma=" . /** @scrutinizer ignore-type */ var_export($chroma, true);
371
        if($noodle)                 $endpoint .= "&noodle=" . /** @scrutinizer ignore-type */ var_export($noodle, true);
372
        if($cinema)                 $endpoint .= "&cinema=" . /** @scrutinizer ignore-type */ var_export($cinema, true);
373
        if($fullSpread)             $endpoint .= "&fullSpread=" . /** @scrutinizer ignore-type */ var_export($fullSpread, true);
374
        if($minBpm)                 $endpoint .= "&minBpm=" . /** @scrutinizer ignore-type */ $minBpm;
375
        if($maxBpm)                 $endpoint .= "&maxBpm=" . /** @scrutinizer ignore-type */ $maxBpm;
376
        if($minNps)                 $endpoint .= "&minNps=" . /** @scrutinizer ignore-type */ $minNps;
377
        if($maxNps)                 $endpoint .= "&maxNps=" . /** @scrutinizer ignore-type */ $maxNps;
378
        if($minRating)              $endpoint .= "&minRating=" . /** @scrutinizer ignore-type */ $minRating;
379
        if($maxRating)              $endpoint .= "&maxRating=" . /** @scrutinizer ignore-type */ $maxRating;
380
        if($minDuration !== null)   $endpoint .= "&minDuration=" . /** @scrutinizer ignore-type */ $minDuration;
381
        if($maxDuration !== null)   $endpoint .= "&maxDuration=" . /** @scrutinizer ignore-type */ $maxDuration;
382
383
        return $this->getMapsByEndpoint($endpoint, $numberOfPage, $startPage);
384
    }
385
386
    ////////////////
387
    /// Get User ///
388
    ////////////////
389
390
    /**
391
     * Private building response functions
392
     * @param string $endpoint
393
     * @return ResponseUser
394
     */
395
    private function getUser(string $endpoint): ResponseUser
396
    {
397
        $response = new ResponseUser();
398
399
        $apiResult = $this->callAPI($endpoint);
400
401
        if($apiResult === false || $apiResult == "Not Found") {
402
            $response->setErrorStatus(true)->setErrorMessage("[getMap] Something went wrong with the API call.");
403
            return $response;
404
        }
405
406
        $response->setUser(new User(json_decode($apiResult)));
407
408
        return $response;
409
    }
410
411
    /**
412
     * Get user's infos by UserID
413
     * @param int $id User ID
414
     * @return ResponseUser
415
     */
416
    public function getUserByID(int $id): ResponseUser
417
    {
418
        return $this->getUser("/users/id/" . $id);
419
    }
420
421
    ////////////////////
422
    /// Download map ///
423
    ////////////////////
424
425
    /**
426
     * Download maps using id (Same as BSR Key)
427
     * @param array $ids Array of maps IDs (Same as BSR Key)
428
     * @param string $targetDir Path to download dir
429
     * @return ResponseDownload
430
     */
431
    public function downloadMapByIds(array $ids, string $targetDir): ResponseDownload
432
    {
433
        return $this->multiQuery->downloadMapZipAndCover($this->multiQuery->buildDownloadArray($ids, false), $targetDir);
434
    }
435
436
    /**
437
     * Download maps using bsr key (Same as ID)
438
     * @param array $keys Array of maps keys (Same as ID)
439
     * @param string $targetDir Path to download dir
440
     * @return ResponseDownload
441
     */
442
    public function downloadMapByKeys(array $keys, string $targetDir): ResponseDownload
443
    {
444
        return $this->multiQuery->downloadMapZipAndCover($this->multiQuery->buildDownloadArray($keys, false), $targetDir);
445
    }
446
447
    /**
448
     * Download maps using hashes
449
     * @param array $hashes
450
     * @param string $targetDir
451
     * @return ResponseDownload
452
     */
453
    public function downloadMapByHashes(array $hashes, string $targetDir): ResponseDownload
454
    {
455
        return $this->multiQuery->downloadMapZipAndCover($this->multiQuery->buildDownloadArray($hashes, true), $targetDir);
456
    }
457
}