Test Setup Failed
Branch master (4b8153)
by Phan
05:07
created

LastfmService   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 354
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 4

Test Coverage

Coverage 62.73%

Importance

Changes 0
Metric Value
wmc 44
lcom 2
cbo 4
dl 0
loc 354
ccs 69
cts 110
cp 0.6273
rs 8.8798
c 0
b 0
f 0

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
A used() 0 4 1
A enabled() 0 4 2
B getArtistInformation() 0 31 6
A buildArtistInformation() 0 11 2
B getAlbumInformation() 0 34 6
A buildAlbumInformation() 0 18 2
A fetchSessionKeyUsingToken() 0 15 2
A scrobble() 0 16 3
A toggleLoveTrack() 0 11 3
A updateNowPlaying() 0 15 3
A buildAuthCallParams() 0 27 4
A formatText() 0 8 2
A getUserSessionKey() 0 4 1
A setUserSessionKey() 0 4 1
A deleteUserSessionKey() 0 4 1
A isUserConnected() 0 4 1
A getKey() 0 4 1
A getEndpoint() 0 4 1
A getSecret() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like LastfmService often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use LastfmService, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace App\Services;
4
5
use App\Models\User;
6
use Exception;
7
use GuzzleHttp\Client;
8
use Illuminate\Contracts\Cache\Repository as Cache;
9
use Illuminate\Log\Logger;
10
11
class LastfmService extends ApiClient implements ApiConsumerInterface
12
{
13
    public const SESSION_KEY_PREFERENCE_KEY = 'lastfm_session_key';
14
15
    /**
16
     * Specify the response format, since Last.fm only returns XML.
17
     *
18
     * @var string
19
     */
20
    protected $responseFormat = 'xml';
21
22
    /**
23
     * Override the key param, since, again, Lastfm wants to be different.
24
     *
25
     * @var string
26
     */
27
    protected $keyParam = 'api_key';
28
29
    private $userPreferenceService;
30
31
    public function __construct(
32
        Client $client,
33
        Cache $cache,
34
        Logger $logger,
35
        UserPreferenceService $userPreferenceService
36
    ) {
37
        parent::__construct($client, $cache, $logger);
38
        $this->userPreferenceService = $userPreferenceService;
39 5
    }
40
41 5
    /**
42
     * Determine if our application is using Last.fm.
43
     */
44
    public function used(): bool
45
    {
46
        return (bool) $this->getKey();
47
    }
48
49
    /**
50
     * Determine if Last.fm integration is enabled.
51 2
     */
52
    public function enabled(): bool
53 2
    {
54
        return $this->getKey() && $this->getSecret();
55
    }
56
57 2
    /**
58
     * Get information about an artist.
59
     *
60 2
     * @param string $name Name of the artist
61
     *
62 2
     * @return mixed[]|null
63
     */
64
    public function getArtistInformation(string $name): ?array
65 2
    {
66 2
        if (!$this->enabled()) {
67
            return null;
68
        }
69
70 2
        $name = urlencode($name);
71
72 2
        try {
73 1
            return $this->cache->remember(md5("lastfm_artist_$name"), 24 * 60 * 7, function () use ($name): ?array {
74
                $response = $this->get("?method=artist.getInfo&autocorrect=1&artist=$name");
0 ignored issues
show
Bug introduced by
The call to get() misses a required argument $...$args.

This check looks for function calls that miss required arguments.

Loading history...
75
76 1
                if (!$response) {
77
                    return null;
78
                }
79
80
                $response = simplexml_load_string($response->asXML());
81
                $response = json_decode(json_encode($response), true);
82
83
                if (!$response || !$artist = array_get($response, 'artist')) {
84
                    return null;
85
                }
86
87
                return $this->buildArtistInformation($artist);
88
            });
89
        } catch (Exception $e) {
90
            $this->logger->error($e);
91 1
92
            return null;
93
        }
94 1
    }
95 1
96
    /**
97 1
     * Build a Koel-usable array of artist information using the data from Last.fm.
98 1
     *
99
     * @param mixed[] $artistData
100
     *
101
     * @return mixed[]
102
     */
103
    private function buildArtistInformation(array $artistData): array
104
    {
105
        return [
106
            'url' => array_get($artistData, 'url'),
107
            'image' => count($artistData['image']) > 3 ? $artistData['image'][3] : $artistData['image'][0],
108
            'bio' => [
109
                'summary' => $this->formatText(array_get($artistData, 'bio.summary', '')),
110
                'full' => $this->formatText(array_get($artistData, 'bio.content', '')),
111 2
            ],
112
        ];
113 2
    }
114
115
    /**
116
     * Get information about an album.
117 2
     *
118 2
     * @return mixed[]|null
119
     */
120
    public function getAlbumInformation(string $albumName, string $artistName): ?array
121 2
    {
122
        if (!$this->enabled()) {
123 2
            return null;
124
        }
125
126 2
        $albumName = urlencode($albumName);
127 2
        $artistName = urlencode($artistName);
128
129
        try {
130
            $cacheKey = md5("lastfm_album_{$albumName}_{$artistName}");
131 2
132
            return $this->cache->remember($cacheKey, 24 * 60 * 7, function () use ($albumName, $artistName): ?array {
133 2
                $response = $this->get("?method=album.getInfo&autocorrect=1&album=$albumName&artist=$artistName");
0 ignored issues
show
Bug introduced by
The call to get() misses a required argument $...$args.

This check looks for function calls that miss required arguments.

Loading history...
134 1
135
                if (!$response) {
136
                    return null;
137 1
                }
138
139
                $response = simplexml_load_string($response->asXML());
140
                $response = json_decode(json_encode($response), true);
141
142
                if (!$response || !$album = array_get($response, 'album')) {
143
                    return null;
144
                }
145
146
                return $this->buildAlbumInformation($album);
147
            });
148
        } catch (Exception $e) {
149
            $this->logger->error($e);
150
151
            return null;
152 1
        }
153
    }
154
155 1
    /**
156 1
     * Build a Koel-usable array of album information using the data from Last.fm.
157
     *
158 1
     * @param mixed[] $albumData
159 1
     *
160
     * @return mixed[]
161 1
     */
162
    private function buildAlbumInformation(array $albumData): array
163 1
    {
164 1
        return [
165 1
            'url' => array_get($albumData, 'url'),
166
            'image' => count($albumData['image']) > 3 ? $albumData['image'][3] : $albumData['image'][0],
167 1
            'wiki' => [
168
                'summary' => $this->formatText(array_get($albumData, 'wiki.summary', '')),
169
                'full' => $this->formatText(array_get($albumData, 'wiki.content', '')),
170
            ],
171
            'tracks' => array_map(function ($track) {
172
                return [
173
                    'title' => $track['name'],
174
                    'length' => (int) $track['duration'],
175
                    'url' => $track['url'],
176
                ];
177
            }, array_get($albumData, 'tracks.track', [])),
178
        ];
179
    }
180 1
181
    /**
182 1
     * Get Last.fm's session key for the authenticated user using a token.
183 1
     *
184 1
     * @param string $token The token after successfully connecting to Last.fm
185 1
     *
186
     * @link http://www.last.fm/api/webauth#4
187
     */
188 1
    public function fetchSessionKeyUsingToken(string $token): ?string
189
    {
190
        $query = $this->buildAuthCallParams([
191
            'method' => 'auth.getSession',
192
            'token' => $token,
193
        ], true);
194
195
        try {
196
            return (string) $this->get("/?$query", [], false)->session->key;
0 ignored issues
show
Unused Code introduced by
The call to LastfmService::get() has too many arguments starting with false.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
197
        } catch (Exception $e) {
198
            $this->logger->error($e);
199
200
            return null;
201
        }
202
    }
203
204
    /**
205
     * Scrobble a song.
206
     *
207
     * @param string     $artist    The artist name
208
     * @param string     $track     The track name
209
     * @param string|int $timestamp The UNIX timestamp
210
     * @param string     $album     The album name
211
     * @param string     $sk        The session key
212
     */
213
    public function scrobble(string $artist, string $track, $timestamp, string $album, string $sk): void
214
    {
215
        $params = compact('artist', 'track', 'timestamp', 'sk');
216
217
        if ($album) {
218
            $params['album'] = $album;
219
        }
220
221
        $params['method'] = 'track.scrobble';
222
223
        try {
224
            $this->post('/', $this->buildAuthCallParams($params), false);
0 ignored issues
show
Unused Code introduced by
The call to LastfmService::post() has too many arguments starting with false.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
225
        } catch (Exception $e) {
226
            $this->logger->error($e);
227
        }
228
    }
229
230
    /**
231
     * Love or unlove a track on Last.fm.
232
     *
233
     * @param string $track  The track name
234
     * @param string $artist The artist's name
235
     * @param string $sk     The session key
236
     * @param bool   $love   Whether to love or unlove. Such cheesy terms... urrgggh
237
     */
238
    public function toggleLoveTrack(string $track, string $artist, string $sk, ?bool $love = true): void
239
    {
240
        $params = compact('track', 'artist', 'sk');
241
        $params['method'] = $love ? 'track.love' : 'track.unlove';
242
243
        try {
244
            $this->post('/', $this->buildAuthCallParams($params), false);
0 ignored issues
show
Unused Code introduced by
The call to LastfmService::post() has too many arguments starting with false.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
245
        } catch (Exception $e) {
246
            $this->logger->error($e);
247
        }
248
    }
249
250
    /**
251
     * Update a track's "now playing" on Last.fm.
252
     *
253
     * @param string    $artist   Name of the artist
254
     * @param string    $track    Name of the track
255
     * @param string    $album    Name of the album
256
     * @param int|float $duration Duration of the track, in seconds
257
     * @param string    $sk       The session key
258
     */
259
    public function updateNowPlaying(string $artist, string $track, string $album, $duration, string $sk): void
260
    {
261
        $params = compact('artist', 'track', 'duration', 'sk');
262
        $params['method'] = 'track.updateNowPlaying';
263
264
        if ($album) {
265
            $params['album'] = $album;
266
        }
267
268
        try {
269
            $this->post('/', $this->buildAuthCallParams($params), false);
0 ignored issues
show
Unused Code introduced by
The call to LastfmService::post() has too many arguments starting with false.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
270
        } catch (Exception $e) {
271
            $this->logger->error($e);
272
        }
273
    }
274
275
    /**
276
     * Build the parameters to use for _authenticated_ Last.fm API calls.
277
     * Such calls require:
278
     * - The API key (api_key)
279
     * - The API signature (api_sig).
280
     *
281
     * @link http://www.last.fm/api/webauth#5
282
     *
283
     * @param array $params   The array of parameters.
284
     * @param bool  $toString Whether to turn the array into a query string
285
     *
286
     * @return array|string
287
     */
288
    public function buildAuthCallParams(array $params, bool $toString = false)
289
    {
290
        $params['api_key'] = $this->getKey();
291
        ksort($params);
292 2
293
        // Generate the API signature.
294 2
        // @link http://www.last.fm/api/webauth#6
295 2
        $str = '';
296
297
        foreach ($params as $name => $value) {
298
            $str .= $name.$value;
299 2
        }
300 2
301 2
        $str .= $this->getSecret();
302
        $params['api_sig'] = md5($str);
303 2
304 2
        if (!$toString) {
305
            return $params;
306 2
        }
307 1
308
        $query = '';
309
        foreach ($params as $key => $value) {
310 2
            $query .= "$key=$value&";
311 2
        }
312 2
313
        return rtrim($query, '&');
314
    }
315 2
316
    /**
317
     * Correctly format a value returned by Last.fm.
318
     *
319
     * @param string|array $value
320
     */
321
    protected function formatText($value): string
322
    {
323
        if (!$value) {
324
            return '';
325 2
        }
326
327 2
        return trim(str_replace('Read more on Last.fm', '', nl2br(strip_tags(html_entity_decode($value)))));
328
    }
329
330
    public function getUserSessionKey(User $user): ?string
331 2
    {
332
        return $this->userPreferenceService->get($user, self::SESSION_KEY_PREFERENCE_KEY);
333
    }
334 6
335
    public function setUserSessionKey(User $user, string $sessionKey): void
336 6
    {
337
        $this->userPreferenceService->set($user, self::SESSION_KEY_PREFERENCE_KEY, $sessionKey);
338
    }
339 5
340
    public function deleteUserSessionKey(User $user): void
341 5
    {
342
        $this->userPreferenceService->delete($user, self::SESSION_KEY_PREFERENCE_KEY);
343
    }
344 6
345
    public function isUserConnected(User $user): bool
346 6
    {
347
        return (bool) $this->getUserSessionKey($user);
348
    }
349
350
    public function getKey(): ?string
351
    {
352
        return config('koel.lastfm.key');
353
    }
354
355
    public function getEndpoint(): ?string
356
    {
357
        return config('koel.lastfm.endpoint');
358
    }
359
360
    public function getSecret(): ?string
361
    {
362
        return config('koel.lastfm.secret');
363
    }
364
}
365