Passed
Push — master ( a0193c...3bf6a5 )
by Pauli
02:17
created

LastfmService   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 210
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 102
c 3
b 0
f 0
dl 0
loc 210
rs 10
wmc 24

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 1
A errorResponse() 0 6 1
A getInfoFromLastFm() 0 32 3
A getArtistInfo() 0 24 4
A getTrackInfo() 0 7 1
B getSimilarArtists() 0 30 6
A getTopTracks() 0 28 6
A getAlbumInfo() 0 10 2
1
<?php declare(strict_types=1);
2
3
/**
4
 * ownCloud - Music app
5
 *
6
 * This file is licensed under the Affero General Public License version 3 or
7
 * later. See the COPYING file.
8
 *
9
 * @author Pauli Järvinen <[email protected]>
10
 * @copyright Pauli Järvinen 2020 - 2022
11
 */
12
13
namespace OCA\Music\Utility;
14
15
use OCA\Music\AppFramework\BusinessLayer\BusinessLayerException;
16
use OCA\Music\AppFramework\Core\Logger;
17
use OCA\Music\BusinessLayer\AlbumBusinessLayer;
18
use OCA\Music\BusinessLayer\ArtistBusinessLayer;
19
use OCA\Music\BusinessLayer\TrackBusinessLayer;
20
use OCA\Music\Db\Artist;
21
use OCA\Music\Db\MatchMode;
22
use OCA\Music\Db\Track;
23
24
use OCP\IConfig;
25
26
class LastfmService {
27
	private $albumBusinessLayer;
28
	private $artistBusinessLayer;
29
	private $trackBusinessLayer;
30
	private $logger;
31
	private $apiKey;
32
33
	const LASTFM_URL = 'http://ws.audioscrobbler.com/2.0/';
34
35
	public function __construct(
36
			AlbumBusinessLayer $albumBusinessLayer,
37
			ArtistBusinessLayer $artistBusinessLayer,
38
			TrackBusinessLayer $trackBusinessLayer,
39
			IConfig $config,
40
			Logger $logger) {
41
		$this->albumBusinessLayer = $albumBusinessLayer;
42
		$this->artistBusinessLayer = $artistBusinessLayer;
43
		$this->trackBusinessLayer = $trackBusinessLayer;
44
		$this->logger = $logger;
45
		$this->apiKey = $config->getSystemValue('music.lastfm_api_key');
46
	}
47
48
	/**
49
	 * @param integer $artistId
50
	 * @param string $userId
51
	 * @return array
52
	 * @throws BusinessLayerException if artist with the given ID is not found
53
	 */
54
	public function getArtistInfo(int $artistId, string $userId) : array {
55
		$artist = $this->artistBusinessLayer->find($artistId, $userId);
56
57
		if ($artist->getName() === null) {
58
			return $this->errorResponse("Can't get details for an unknown artist");
59
		} else {
60
			$result = $this->getInfoFromLastFm([
61
				'method' => 'artist.getInfo',
62
				'artist' => $artist->getName()
63
			]);
64
65
			// add ID to those similar artists which can be found from the library
66
			$similar = $result['artist']['similar']['artist'] ?? null;
67
			if ($similar !== null) {
68
				$result['artist']['similar']['artist'] = \array_map(function ($lastfmArtist) use ($userId) {
69
					$matching = $this->artistBusinessLayer->findAllByName($lastfmArtist['name'], $userId);
70
					if (!empty($matching)) {
71
						$lastfmArtist['id'] = $matching[0]->getId();
72
					}
73
					return $lastfmArtist;
74
				}, $similar);
75
			}
76
77
			return $result;
78
		}
79
	}
80
81
	/**
82
	 * @param integer $albumId
83
	 * @param string $userId
84
	 * @return array
85
	 * @throws BusinessLayerException if album with the given ID is not found
86
	 */
87
	public function getAlbumInfo(int $albumId, string $userId) : array {
88
		$album = $this->albumBusinessLayer->find($albumId, $userId);
89
90
		if ($album->getName() === null) {
0 ignored issues
show
introduced by
The condition $album->getName() === null is always false.
Loading history...
91
			return $this->errorResponse("Can't get details for an unknown album");
92
		} else {
93
			return $this->getInfoFromLastFm([
94
				'method' => 'album.getInfo',
95
				'artist' => $album->getAlbumArtistName(),
96
				'album' => $album->getName()
97
			]);
98
		}
99
	}
100
101
	/**
102
	 * @param integer $trackId
103
	 * @param string $userId
104
	 * @return array
105
	 * @throws BusinessLayerException if track with the given ID is not found
106
	 */
107
	public function getTrackInfo(int $trackId, string $userId) : array {
108
		$track= $this->trackBusinessLayer->find($trackId, $userId);
109
110
		return $this->getInfoFromLastFm([
111
				'method' => 'track.getInfo',
112
				'artist' => $track->getArtistName(),
113
				'track' => $track->getTitle()
114
		]);
115
	}
116
117
	/**
118
	 * Get artists from the user's library similar to the given artist
119
	 * @param integer $artistId
120
	 * @param string $userId
121
	 * @parma bool $includeNotPresent When true, the result may include also artists which
122
	 *                                are not found from the user's music library. Such
123
	 *                                artists have many fields including `id` set as null.
124
	 * @return Artist[]
125
	 * @throws BusinessLayerException if artist with the given ID is not found
126
	 */
127
	public function getSimilarArtists(int $artistId, string $userId, $includeNotPresent=false) : array {
128
		$artist = $this->artistBusinessLayer->find($artistId, $userId);
129
130
		$similarOnLastfm = $this->getInfoFromLastFm([
131
			'method' => 'artist.getSimilar',
132
			'artist' => $artist->getName()
133
		]);
134
135
		$result = [];
136
		$similarArr = $similarOnLastfm['similarartists']['artist'] ?? null;
137
		if ($similarArr !== null) {
138
			foreach ($similarArr as $lastfmArtist) {
139
				$matchingLibArtists = $this->artistBusinessLayer->findAllByName($lastfmArtist['name'], $userId);
140
141
				if (!empty($matchingLibArtists)) {
142
					foreach ($matchingLibArtists as &$matchArtist) { // loop although there really shouldn't be more than one
143
						$matchArtist->setLastfmUrl($lastfmArtist['url']);
144
					}
145
					$result = \array_merge($result, $matchingLibArtists);
146
				} elseif ($includeNotPresent) {
147
					$unfoundArtist = new Artist();
148
					$unfoundArtist->setName($lastfmArtist['name'] ?? null);
149
					$unfoundArtist->setMbid($lastfmArtist['mbid'] ?? null);
150
					$unfoundArtist->setLastfmUrl($lastfmArtist['url'] ?? null);
151
					$result[] = $unfoundArtist;
152
				}
153
			}
154
		}
155
156
		return $result;
157
	}
158
159
	/**
160
	 * Get artist tracks from the user's library, sorted by their popularity on Last.fm
161
	 * @param int $maxCount Number of tracks to request from Last.fm. Note that the function may return much
162
	 *						less tracks if the top tracks from Last.fm are not present in the user's library.
163
	 * @return Track[]
164
	 */
165
	public function getTopTracks(string $artistName, string $userId, int $maxCount) : array {
166
		$foundTracks = [];
167
168
		$artist = $this->artistBusinessLayer->findAllByName($artistName, $userId, MatchMode::Exact, /*$limit=*/1)[0] ?? null;
169
170
		if ($artist !== null) {
171
			$lastfmResult = $this->getInfoFromLastFm([
172
				'method' => 'artist.getTopTracks',
173
				'artist' => $artistName,
174
				'limit' => $maxCount
175
			]);
176
			$topTracksOnLastfm = $lastfmResult['toptracks']['track'] ?? null;
177
178
			if ($topTracksOnLastfm !== null) {
179
				$libTracks = $this->trackBusinessLayer->findAllByArtist($artist->getId(), $userId);
180
181
				foreach ($topTracksOnLastfm as $lastfmTrack) {
182
					foreach ($libTracks as $libTrack) {
183
						if (\mb_strtolower($lastfmTrack['name']) === \mb_strtolower($libTrack->getTitle())) {
184
							$foundTracks[] = $libTrack;
185
							break;
186
						}
187
					}
188
				}
189
			}
190
		}
191
192
		return $foundTracks;
193
	}
194
195
	private function getInfoFromLastFm(array $args) : array {
196
		if (empty($this->apiKey)) {
197
			return ['api_key_set' => false];
198
		} else {
199
			// append the standard args
200
			$args['api_key'] = $this->apiKey;
201
			$args['format'] = 'json';
202
203
			// remove args with null or empty values
204
			$args = \array_filter($args, [Util::class, 'isNonEmptyString']);
205
206
			// glue arg keys and values together ...
207
			$args = \array_map(function ($key, $value) {
208
				return $key . '=' . \urlencode($value);
209
			}, \array_keys($args), $args);
210
			// ... and form the final query string
211
			$queryString = '?' . \implode('&', $args);
212
213
			list('content' => $info, 'status_code' => $statusCode, 'message' => $msg) = HttpUtil::loadFromUrl(self::LASTFM_URL . $queryString);
214
215
			if ($info === false) {
216
				// When an album is not found, Last.fm returns 404 but that is not a sign of broken connection.
217
				// Interestingly, not finding an artist is still responded with the code 200.
218
				$info = ['connection_ok' => ($statusCode === 404)];
219
			} else {
220
				$info = \json_decode($info, true);
221
				$info['connection_ok'] = true;
222
			}
223
			$info['status_code'] = $statusCode;
224
			$info['status_msg'] = $msg;
225
			$info['api_key_set'] = true;
226
			return $info;
227
		}
228
	}
229
230
	private function errorResponse(string $message) : array {
231
		return [
232
			'api_key_set' => !empty($this->apiKey),
233
			'connection_ok' => 'unknown',
234
			'status_code' => -1,
235
			'status_msg' => $message
236
		];
237
	}
238
}
239