Passed
Push — master ( ec8b1a...0c5029 )
by Kylian
01:50
created

BeatSaverAPI::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
c 1
b 0
f 0
nc 2
nop 2
dl 0
loc 7
rs 10
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
}