Passed
Push — feature/909_Ampache_API_improv... ( b51dbd...10355f )
by Pauli
02:39
created

LastfmService::getTopTracks()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 32
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 7
eloc 19
c 2
b 0
f 0
nc 6
nop 3
dl 0
loc 32
rs 8.8333
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 - 2023
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) {
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
	 * @param 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 tracks from the user's library similar to the given track
161
	 * @return Track[]
162
	 * @throws BusinessLayerException if track with the given ID is not found
163
	 */
164
	public function getSimilarTracks(int $trackId, string $userId) : array {
165
		$track = $this->trackBusinessLayer->find($trackId, $userId);
166
167
		$similarOnLastfm = $this->getInfoFromLastFm([
168
			'method' => 'track.getSimilar',
169
			'track' => $track->getTitle(),
170
			'artist' => $track->getArtistName()
171
		]);
172
173
		$result = [];
174
		$similarArr = $similarOnLastfm['similartracks']['track'] ?? null;
175
		if ($similarArr !== null) {
176
			foreach ($similarArr as $lastfmTrack) {
177
				$matchingLibTracks = $this->trackBusinessLayer->findAllByNameAndArtistName($lastfmTrack['name'], $lastfmTrack['artist']['name'], $userId);
178
				$result = \array_merge($result, $matchingLibTracks);
179
			}
180
		}
181
182
		return $result;
183
	}
184
185
	/**
186
	 * Get artist tracks from the user's library, sorted by their popularity on Last.fm
187
	 * @param int|string $artistIdOrName Either the ID of the artist or the artist's name written exectly
188
	 * 									like in the DB. Any integer-typed value is treated as an ID and 
189
	 * 									string-typed value as a name.
190
	 * @param int $maxCount Number of tracks to request from Last.fm. Note that the function may return much
191
	 *						less tracks if the top tracks from Last.fm are not present in the user's library.
192
	 * @return Track[]
193
	 */
194
	public function getTopTracks(/*mixed*/ $artistIdOrName, string $userId, int $maxCount) : array {
195
		$foundTracks = [];
196
197
		if (\is_integer($artistIdOrName)) {
198
			$artist = $this->artistBusinessLayer->find($artistIdOrName, $userId);
199
		} else {
200
			$artist = $this->artistBusinessLayer->findAllByName($artistIdOrName, $userId, MatchMode::Exact, /*$limit=*/1)[0] ?? null;
201
		}
202
203
		if ($artist !== null) {
204
			$lastfmResult = $this->getInfoFromLastFm([
205
				'method' => 'artist.getTopTracks',
206
				'artist' => $artist->getName(),
207
				'limit' => (string)$maxCount
208
			]);
209
			$topTracksOnLastfm = $lastfmResult['toptracks']['track'] ?? null;
210
211
			if ($topTracksOnLastfm !== null) {
212
				$libTracks = $this->trackBusinessLayer->findAllByArtist($artist->getId(), $userId);
213
214
				foreach ($topTracksOnLastfm as $lastfmTrack) {
215
					foreach ($libTracks as $libTrack) {
216
						if (\mb_strtolower($lastfmTrack['name']) === \mb_strtolower($libTrack->getTitle())) {
217
							$foundTracks[] = $libTrack;
218
							break;
219
						}
220
					}
221
				}
222
			}
223
		}
224
225
		return $foundTracks;
226
	}
227
228
	private function getInfoFromLastFm(array $args) : array {
229
		if (empty($this->apiKey)) {
230
			return ['api_key_set' => false];
231
		} else {
232
			// append the standard args
233
			$args['api_key'] = $this->apiKey;
234
			$args['format'] = 'json';
235
236
			// remove args with null or empty values
237
			$args = \array_filter($args, [Util::class, 'isNonEmptyString']);
238
239
			// glue arg keys and values together ...
240
			$args = \array_map(function ($key, $value) {
241
				return $key . '=' . \urlencode($value);
242
			}, \array_keys($args), $args);
243
			// ... and form the final query string
244
			$queryString = '?' . \implode('&', $args);
245
246
			list('content' => $info, 'status_code' => $statusCode, 'message' => $msg) = HttpUtil::loadFromUrl(self::LASTFM_URL . $queryString);
247
248
			if ($info === false) {
249
				// When an album is not found, Last.fm returns 404 but that is not a sign of broken connection.
250
				// Interestingly, not finding an artist is still responded with the code 200.
251
				$info = ['connection_ok' => ($statusCode === 404)];
252
			} else {
253
				$info = \json_decode($info, true);
254
				$info['connection_ok'] = true;
255
			}
256
			$info['status_code'] = $statusCode;
257
			$info['status_msg'] = $msg;
258
			$info['api_key_set'] = true;
259
			return $info;
260
		}
261
	}
262
263
	private function errorResponse(string $message) : array {
264
		return [
265
			'api_key_set' => !empty($this->apiKey),
266
			'connection_ok' => 'unknown',
267
			'status_code' => -1,
268
			'status_msg' => $message
269
		];
270
	}
271
}
272