Passed
Push — master ( b75966...f85fb3 )
by Pauli
10:39
created

LastfmService::fetchUrl()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 8
rs 10
cc 1
nc 1
nop 1
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
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
		$result = $this->getInfoFromLastFm([
58
				'method' => 'artist.getInfo',
59
				'artist' => $artist->getName()
60
		]);
61
62
		// add ID to those similar artists which can be found from the library
63
		$similar = $result['artist']['similar']['artist'] ?? null;
64
		if ($similar !== null) {
65
			$result['artist']['similar']['artist'] = \array_map(function ($lastfmArtist) use ($userId) {
66
				$matching = $this->artistBusinessLayer->findAllByName($lastfmArtist['name'], $userId);
67
				if (!empty($matching)) {
68
					$lastfmArtist['id'] = $matching[0]->getId();
69
				}
70
				return $lastfmArtist;
71
			}, $similar);
72
		}
73
74
		return $result;
75
	}
76
77
	/**
78
	 * @param integer $albumId
79
	 * @param string $userId
80
	 * @return array
81
	 * @throws BusinessLayerException if album with the given ID is not found
82
	 */
83
	public function getAlbumInfo(int $albumId, string $userId) : array {
84
		$album = $this->albumBusinessLayer->find($albumId, $userId);
85
86
		return $this->getInfoFromLastFm([
87
				'method' => 'album.getInfo',
88
				'artist' => $album->getAlbumArtistName(),
89
				'album' => $album->getName()
90
		]);
91
	}
92
93
	/**
94
	 * @param integer $trackId
95
	 * @param string $userId
96
	 * @return array
97
	 * @throws BusinessLayerException if track with the given ID is not found
98
	 */
99
	public function getTrackInfo(int $trackId, string $userId) : array {
100
		$track= $this->trackBusinessLayer->find($trackId, $userId);
101
102
		return $this->getInfoFromLastFm([
103
				'method' => 'track.getInfo',
104
				'artist' => $track->getArtistName(),
105
				'track' => $track->getTitle()
106
		]);
107
	}
108
109
	/**
110
	 * Get artists from the user's library similar to the given artist
111
	 * @param integer $artistId
112
	 * @param string $userId
113
	 * @parma bool $includeNotPresent When true, the result may include also artists which
114
	 *                                are not found from the user's music library. Such
115
	 *                                artists have many fields including `id` set as null.
116
	 * @return Artist[]
117
	 * @throws BusinessLayerException if artist with the given ID is not found
118
	 */
119
	public function getSimilarArtists(int $artistId, string $userId, $includeNotPresent=false) : array {
120
		$artist = $this->artistBusinessLayer->find($artistId, $userId);
121
122
		$similarOnLastfm = $this->getInfoFromLastFm([
123
			'method' => 'artist.getSimilar',
124
			'artist' => $artist->getName()
125
		]);
126
127
		$result = [];
128
		$similarArr = $similarOnLastfm['similarartists']['artist'] ?? null;
129
		if ($similarArr !== null) {
130
			foreach ($similarArr as $lastfmArtist) {
131
				$matchingLibArtists = $this->artistBusinessLayer->findAllByName($lastfmArtist['name'], $userId);
132
133
				if (!empty($matchingLibArtists)) {
134
					foreach ($matchingLibArtists as &$matchArtist) { // loop although there really shouldn't be more than one
135
						$matchArtist->setLastfmUrl($lastfmArtist['url']);
136
					}
137
					$result = \array_merge($result, $matchingLibArtists);
138
				} elseif ($includeNotPresent) {
139
					$unfoundArtist = new Artist();
140
					$unfoundArtist->setName($lastfmArtist['name'] ?? null);
141
					$unfoundArtist->setMbid($lastfmArtist['mbid'] ?? null);
142
					$unfoundArtist->setLastfmUrl($lastfmArtist['url'] ?? null);
143
					$result[] = $unfoundArtist;
144
				}
145
			}
146
		}
147
148
		return $result;
149
	}
150
151
	/**
152
	 * Get artist tracks from the user's library, sorted by their popularity on Last.fm
153
	 * @param int $maxCount Number of tracks to request from Last.fm. Note that the function may return much
154
	 *						less tracks if the top tracks from Last.fm are not present in the user's library.
155
	 * @return Track[]
156
	 */
157
	public function getTopTracks(string $artistName, string $userId, int $maxCount) : array {
158
		$foundTracks = [];
159
160
		$artist = $this->artistBusinessLayer->findAllByName($artistName, $userId, MatchMode::Exact, /*$limit=*/1)[0] ?? null;
161
162
		if ($artist !== null) {
163
			$lastfmResult = $this->getInfoFromLastFm([
164
				'method' => 'artist.getTopTracks',
165
				'artist' => $artistName,
166
				'limit' => $maxCount
167
			]);
168
			$topTracksOnLastfm = $lastfmResult['toptracks']['track'] ?? null;
169
170
			if ($topTracksOnLastfm !== null) {
171
				$libTracks = $this->trackBusinessLayer->findAllByArtist($artist->getId(), $userId);
172
173
				foreach ($topTracksOnLastfm as $lastfmTrack) {
174
					foreach ($libTracks as $libTrack) {
175
						if (\mb_strtolower($lastfmTrack['name']) === \mb_strtolower($libTrack->getTitle())) {
176
							$foundTracks[] = $libTrack;
177
							break;
178
						}
179
					}
180
				}
181
			}
182
		}
183
184
		return $foundTracks;
185
	}
186
187
	private function getInfoFromLastFm($args) {
188
		if (empty($this->apiKey)) {
189
			return ['api_key_set' => false];
190
		} else {
191
			// append the standard args
192
			$args['api_key'] = $this->apiKey;
193
			$args['format'] = 'json';
194
195
			// remove args with null or empty values
196
			$args = \array_filter($args, [Util::class, 'isNonEmptyString']);
197
198
			// glue arg keys and values together ...
199
			$args = \array_map(function ($key, $value) {
200
				return $key . '=' . \urlencode($value);
201
			}, \array_keys($args), $args);
202
			// ... and form the final query string
203
			$queryString = '?' . \implode('&', $args);
204
205
			list('content' => $info, 'status_code' => $statusCode, 'message' => $msg) = HttpUtil::loadFromUrl(self::LASTFM_URL . $queryString);
206
207
			if ($info === false) {
208
				// When an album is not found, Last.fm returns 404 but that is not a sign of broken connection.
209
				// Interestingly, not finding an artist is still responded with the code 200.
210
				$info = ['connection_ok' => ($statusCode === 404)];
211
			} else {
212
				$info = \json_decode($info, true);
213
				$info['connection_ok'] = true;
214
			}
215
			$info['status_code'] = $statusCode;
216
			$info['status_msg'] = $msg;
217
			$info['api_key_set'] = true;
218
			return $info;
219
		}
220
	}
221
}
222