Passed
Push — master ( 163cac...2d8094 )
by Kylian
07:24 queued 06:00
created

BeatSaverAPI::downloadMapByHashes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 2
dl 0
loc 3
rs 10
c 0
b 0
f 0
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
17
    private string $userAgent;
18
    private MultiQuery $multiQuery;
19
20
    /**
21
     * BeatSaverAPI constructor
22
     * @param string $userAgent User Agent to provide to Beat Saver API
23
     */
24
    public function __construct(string $userAgent)
25
    {
26
        $this->autoload("./");
27
28
        $this->userAgent = $userAgent;
29
        $this->multiQuery = new MultiQuery(self::BEATSAVER_URL, $userAgent);
30
    }
31
32
    private function autoload($directory) {
33
        if(is_dir($directory)) {
34
            $scan = scandir($directory);
35
            unset($scan[0], $scan[1]); //unset . and ..
36
            foreach($scan as $file) {
37
                if(is_dir($directory."/".$file)) {
38
                    $this->autoload($directory."/".$file);
39
                } else {
40
                    if(strpos($file, '.php') !== false) {
41
                        include_once($directory."/".$file);
42
                    }
43
                }
44
            }
45
        }
46
    }
47
48
    /**
49
     * Private calling API function
50
     * @param string $endpoint
51
     * @return string|null
52
     */
53
    private function callAPI(string $endpoint): ?string
54
    {
55
        $curl = curl_init();
56
57
        curl_setopt_array($curl, [
58
            CURLOPT_URL => self::BEATSAVER_URL . $endpoint,
59
            CURLOPT_USERAGENT => $this->userAgent,
60
            CURLOPT_RETURNTRANSFER => true
61
        ]);
62
63
        $result = curl_exec($curl);
64
65
        curl_close($curl);
66
67
        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...
68
    }
69
70
    ////////////
71
    /// CALL ///
72
    ///////////
73
74
    /**
75
     * Private building response functions
76
     * @param string $endpoint
77
     * @return ResponseMap
78
     */
79
    private function getMap(string $endpoint): ResponseMap
80
    {
81
        $response = new ResponseMap();
82
83
        $apiResult = $this->callAPI($endpoint);
84
85
        if($apiResult === false || $apiResult == "Not Found") {
86
            $response->setErrorStatus(true)->setErrorStatus("[getMap] Something went wrong with the API call.");
0 ignored issues
show
Bug introduced by
'[getMap] Something went...ong with the API call.' of type string is incompatible with the type boolean expected by parameter $errorStatus of KriKrixs\object\response...ponse::setErrorStatus(). ( Ignorable by Annotation )

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

86
            $response->setErrorStatus(true)->setErrorStatus(/** @scrutinizer ignore-type */ "[getMap] Something went wrong with the API call.");
Loading history...
87
            return $response;
88
        }
89
90
        $response->setBeatMap(new BeatMap(json_decode($apiResult)));
91
92
        return $response;
93
    }
94
95
    ///////////////
96
    /// Get Map ///
97
    ///////////////
98
99
    /**
100
     * Get map by ID (Same as BSR Key)
101
     * @param string $id Map ID
102
     * @return ResponseMap
103
     */
104
    public function getMapByID(string $id): ResponseMap
105
    {
106
        return $this->getMap("/maps/id/" . $id);
107
    }
108
109
    /**
110
     * Get map by BSR Key (Same as ID)
111
     * @param string $bsrKey Map BSR key
112
     * @return ResponseMap
113
     */
114
    public function getMapByKey(string $bsrKey): ResponseMap
115
    {
116
        return $this->getMap("/maps/id/" . $bsrKey);
117
    }
118
119
    /**
120
     * Get map by Hash
121
     * @param string $hash Hash of the map
122
     * @return ResponseMap
123
     */
124
    public function getMapByHash(string $hash): ResponseMap
125
    {
126
        return $this->getMap("/maps/hash/" . $hash);
127
    }
128
129
    ////////////////
130
    /// Get Maps ///
131
    ////////////////
132
133
    /**
134
     * Private building response functions
135
     * @param string $endpoint
136
     * @param int $numberOfPage
137
     * @param int $startPage
138
     * @return ResponseMaps
139
     */
140
    private function getMaps(string $endpoint, int $numberOfPage = 0, int $startPage = 0): ResponseMaps
141
    {
142
        $response = new ResponseMaps();
143
        $maps = [];
144
145
        // Latest
146
        if($numberOfPage === 0 && $startPage === 0){
147
            $apiResult = json_decode($this->callAPI($endpoint));
148
149
            if($apiResult === false || $apiResult == "Not Found") {
150
                $response->setErrorStatus(true)->setErrorMessage("[getMaps] Something went wrong with the API call while calling the first page.");
151
                return $response;
152
            } else{
153
                foreach ($apiResult->docs as $beatmap) {
154
                    $maps[] = new BeatMap($beatmap);
155
                }
156
            }
157
        } else {
158
            for($i = $startPage; $i < ($i + $numberOfPage); $i++){
159
                $apiResult = json_decode($this->callAPI(str_ireplace("page", $i, $endpoint)));
0 ignored issues
show
Bug introduced by
It seems like str_ireplace('page', $i, $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

159
                $apiResult = json_decode($this->callAPI(/** @scrutinizer ignore-type */ str_ireplace("page", $i, $endpoint)));
Loading history...
160
161
                if($apiResult === false || $apiResult == "Not Found") {
162
                    $response->setErrorStatus(true)->setErrorMessage("[getMaps] Something went wrong with the API call while calling page number " . $i . ".");
163
164
                    if($apiResult == "Not Found")
165
                        return $response;
166
                }
167
168
                foreach ($apiResult->docs as $beatmap) {
169
                    $maps[] = new BeatMap($beatmap);
170
                }
171
            }
172
        }
173
174
        $response->setBeatMaps($maps);
175
176
        return $response;
177
    }
178
179
    /**
180
     * Get maps by Uploader ID! Not the uploader name!
181
     * @param int $uploaderID Uploader ID on BeatSaver
182
     * @param int $numberOfPage The number of page you want to be returned
183
     * @param int $startPage The starting page
184
     * @return ResponseMaps
185
     */
186
    public function getMapsByUploaderID(int $uploaderID, int $numberOfPage, int $startPage): ResponseMaps
187
    {
188
        return $this->getMaps("/maps/uploader/" . $uploaderID . "/page", $numberOfPage, $startPage);
189
    }
190
191
    /**
192
     * Get 20 latest maps
193
     * @param bool $autoMapper Do you want automapper or not ?
194
     * @return ResponseMaps
195
     */
196
    public function getMapsSortedByLatest(bool $autoMapper): ResponseMaps
197
    {
198
        return $this->getMaps("/maps/latest?automapper=" . var_export($autoMapper, true));
199
    }
200
201
    /**
202
     * Get maps sorted by plays numbers
203
     * @param int $numberOfPage The number of page you want to be returned
204
     * @param int $startPage The starting page
205
     * @return ResponseMaps
206
     */
207
    public function getMapsSortedByPlays(int $numberOfPage, int $startPage): ResponseMaps
208
    {
209
        return $this->getMaps("/maps/plays/page", $numberOfPage, $startPage);
210
    }
211
212
    /**
213
     * Search a map (Set null to a parameter to not use it)
214
     * @param int $limit Limit of map you want
215
     * @param int $sortOrder (Default 1) 1 = Latest | 2 = Relevance | 3 = Rating
216
     * @param string|null $mapName (Optional) Map name
217
     * @param \DateTime|null $startDate (Optional) Map made from this date
218
     * @param \DateTime|null $endDate (Optional) Map made to this date
219
     * @param bool $ranked (Optional) Want ranked or not ?
220
     * @param bool $automapper (Optional) Want automapper or not ?
221
     * @param bool $chroma (Optional) Want chroma or not ?
222
     * @param bool $noodle (Optional) Want noodle or not ?
223
     * @param bool $cinema (Optional) Want cinema or not ?
224
     * @param bool $fullSpread (Optional) Want fullSpread or not ?
225
     * @param float|null $minBpm (Optional) Minimum BPM
226
     * @param float|null $maxBpm (Optional) Maximum BPM
227
     * @param float|null $minNps (Optional) Minimum NPS
228
     * @param float|null $maxNps (Optional) Maximum NPS
229
     * @param float|null $minRating (Optional) Minimum Rating
230
     * @param float|null $maxRating (Optional) Maximum Rating
231
     * @param int|null $minDuration (Optional) Minimum Duration
232
     * @param int|null $maxDuration (Optional) Maximum Duration
233
     * @return ResponseMaps
234
     */
235
    public function searchMap(int $limit, 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
236
    {
237
        $sort = [
238
            1 => "Latest",
239
            2 => "Relevance",
240
            3 => "Rating"
241
        ];
242
243
        $endpoint = "/search/text/page?sortOrder=" . $sort[$sortOrder];
244
245
        if($mapName)                $endpoint .= "&q=" . urlencode($mapName);
246
        if($startDate)              $endpoint .= "&from=" . $startDate->format("Y-m-d");
247
        if($endDate)                $endpoint .= "&to=" . $endDate->format("Y-m-d");
248
        if($ranked)                 $endpoint .= "&ranked=" . /** @scrutinizer ignore-type */ var_export($ranked, true);
249
        if($automapper)             $endpoint .= "&automapper=" . /** @scrutinizer ignore-type */ var_export($automapper, true);
250
        if($chroma)                 $endpoint .= "&chroma=" . /** @scrutinizer ignore-type */ var_export($chroma, true);
251
        if($noodle)                 $endpoint .= "&noodle=" . /** @scrutinizer ignore-type */ var_export($noodle, true);
252
        if($cinema)                 $endpoint .= "&cinema=" . /** @scrutinizer ignore-type */ var_export($cinema, true);
253
        if($fullSpread)             $endpoint .= "&fullSpread=" . /** @scrutinizer ignore-type */ var_export($fullSpread, true);
254
        if($minBpm)                 $endpoint .= "&minBpm=" . /** @scrutinizer ignore-type */ $minBpm;
255
        if($maxBpm)                 $endpoint .= "&maxBpm=" . /** @scrutinizer ignore-type */ $maxBpm;
256
        if($minNps)                 $endpoint .= "&minNps=" . /** @scrutinizer ignore-type */ $minNps;
257
        if($maxNps)                 $endpoint .= "&maxNps=" . /** @scrutinizer ignore-type */ $maxNps;
258
        if($minRating)              $endpoint .= "&minRating=" . /** @scrutinizer ignore-type */ $minRating;
259
        if($maxRating)              $endpoint .= "&maxRating=" . /** @scrutinizer ignore-type */ $maxRating;
260
        if($minDuration !== null)   $endpoint .= "&minDuration=" . /** @scrutinizer ignore-type */ $minDuration;
261
        if($maxDuration !== null)   $endpoint .= "&maxDuration=" . /** @scrutinizer ignore-type */ $maxDuration;
262
263
        return $this->getMaps($endpoint, $limit);
264
    }
265
266
    ////////////////
267
    /// Get User ///
268
    ////////////////
269
270
    /**
271
     * Private building response functions
272
     * @param string $endpoint
273
     * @return ResponseUser
274
     */
275
    private function getUser(string $endpoint): ResponseUser
276
    {
277
        $response = new ResponseUser();
278
279
        $apiResult = $this->callAPI($endpoint);
280
281
        if($apiResult === false || $apiResult == "Not Found") {
282
            $response->setErrorStatus(true)->setErrorStatus("[getMap] Something went wrong with the API call.");
0 ignored issues
show
Bug introduced by
'[getMap] Something went...ong with the API call.' of type string is incompatible with the type boolean expected by parameter $errorStatus of KriKrixs\object\response...ponse::setErrorStatus(). ( Ignorable by Annotation )

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

282
            $response->setErrorStatus(true)->setErrorStatus(/** @scrutinizer ignore-type */ "[getMap] Something went wrong with the API call.");
Loading history...
283
            return $response;
284
        }
285
286
        $response->setUser(new User(json_decode($apiResult)));
287
288
        return $response;
289
    }
290
291
    /**
292
     * Get user's infos by UserID
293
     * @param int $id User ID
294
     * @return ResponseUser
295
     */
296
    public function getUserByID(int $id): ResponseUser
297
    {
298
        return $this->getUser("/users/id/" . $id);
299
    }
300
301
    ////////////////////
302
    /// Download map ///
303
    ////////////////////
304
305
    /**
306
     * Download maps using id
307
     * @param array $ids
308
     * @param string $targetDir
309
     * @return ResponseDownload
310
     */
311
    public function downloadMapByIds(array $ids, string $targetDir): ResponseDownload
312
    {
313
        return $this->multiQuery->downloadMapZipAndCover($this->multiQuery->buildDownloadArray($ids, false), $targetDir);
314
    }
315
316
    /**
317
     * Download maps using hashes
318
     * @param array $hashes
319
     * @param string $targetDir
320
     * @return ResponseDownload
321
     */
322
    public function downloadMapByHashes(array $hashes, string $targetDir): ResponseDownload
323
    {
324
        return $this->multiQuery->downloadMapZipAndCover($this->multiQuery->buildDownloadArray($hashes, true), $targetDir);
325
    }
326
}