Passed
Push — master ( 731973...591be8 )
by Pauli
02:33
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\Track;
22
23
use \OCP\IConfig;
24
25
class LastfmService {
26
	private $albumBusinessLayer;
27
	private $artistBusinessLayer;
28
	private $trackBusinessLayer;
29
	private $logger;
30
	private $apiKey;
31
32
	const LASTFM_URL = 'http://ws.audioscrobbler.com/2.0/';
33
34
	public function __construct(
35
			AlbumBusinessLayer $albumBusinessLayer,
36
			ArtistBusinessLayer $artistBusinessLayer,
37
			TrackBusinessLayer $trackBusinessLayer,
38
			IConfig $config,
39
			Logger $logger) {
40
		$this->albumBusinessLayer = $albumBusinessLayer;
41
		$this->artistBusinessLayer = $artistBusinessLayer;
42
		$this->trackBusinessLayer = $trackBusinessLayer;
43
		$this->logger = $logger;
44
		$this->apiKey = $config->getSystemValue('music.lastfm_api_key');
45
	}
46
47
	/**
48
	 * @param integer $artistId
49
	 * @param string $userId
50
	 * @return array
51
	 * @throws BusinessLayerException if artist with the given ID is not found
52
	 */
53
	public function getArtistInfo(int $artistId, string $userId) : array {
54
		$artist = $this->artistBusinessLayer->find($artistId, $userId);
55
56
		$result = $this->getInfoFromLastFm([
57
				'method' => 'artist.getInfo',
58
				'artist' => $artist->getName()
59
		]);
60
61
		// add ID to those similar artists which can be found from the library
62
		$similar = $result['artist']['similar']['artist'] ?? null;
63
		if ($similar !== null) {
64
			$result['artist']['similar']['artist'] = \array_map(function ($lastfmArtist) use ($userId) {
65
				$matching = $this->artistBusinessLayer->findAllByName($lastfmArtist['name'], $userId);
66
				if (!empty($matching)) {
67
					$lastfmArtist['id'] = $matching[0]->getId();
68
				}
69
				return $lastfmArtist;
70
			}, $similar);
71
		}
72
73
		return $result;
74
	}
75
76
	/**
77
	 * @param integer $albumId
78
	 * @param string $userId
79
	 * @return array
80
	 * @throws BusinessLayerException if album with the given ID is not found
81
	 */
82
	public function getAlbumInfo(int $albumId, string $userId) : array {
83
		$album = $this->albumBusinessLayer->find($albumId, $userId);
84
85
		return $this->getInfoFromLastFm([
86
				'method' => 'album.getInfo',
87
				'artist' => $album->getAlbumArtistName(),
88
				'album' => $album->getName()
89
		]);
90
	}
91
92
	/**
93
	 * @param integer $trackId
94
	 * @param string $userId
95
	 * @return array
96
	 * @throws BusinessLayerException if track with the given ID is not found
97
	 */
98
	public function getTrackInfo(int $trackId, string $userId) : array {
99
		$track= $this->trackBusinessLayer->find($trackId, $userId);
100
101
		return $this->getInfoFromLastFm([
102
				'method' => 'track.getInfo',
103
				'artist' => $track->getArtistName(),
104
				'track' => $track->getTitle()
105
		]);
106
	}
107
108
	/**
109
	 * Get artists from the user's library similar to the given artist
110
	 * @param integer $artistId
111
	 * @param string $userId
112
	 * @parma bool $includeNotPresent When true, the result may include also artists which
113
	 *                                are not found from the user's music library. Such
114
	 *                                artists have many fields including `id` set as null.
115
	 * @return Artist[]
116
	 * @throws BusinessLayerException if artist with the given ID is not found
117
	 */
118
	public function getSimilarArtists(int $artistId, string $userId, $includeNotPresent=false) : array {
119
		$artist = $this->artistBusinessLayer->find($artistId, $userId);
120
121
		$similarOnLastfm = $this->getInfoFromLastFm([
122
			'method' => 'artist.getSimilar',
123
			'artist' => $artist->getName()
124
		]);
125
126
		$result = [];
127
		$similarArr = $similarOnLastfm['similarartists']['artist'] ?? null;
128
		if ($similarArr !== null) {
129
			foreach ($similarArr as $lastfmArtist) {
130
				$matchingLibArtists = $this->artistBusinessLayer->findAllByName($lastfmArtist['name'], $userId);
131
132
				if (!empty($matchingLibArtists)) {
133
					foreach ($matchingLibArtists as &$matchArtist) { // loop although there really shouldn't be more than one
134
						$matchArtist->setLastfmUrl($lastfmArtist['url']);
135
					}
136
					$result = \array_merge($result, $matchingLibArtists);
137
				} elseif ($includeNotPresent) {
138
					$unfoundArtist = new Artist();
139
					$unfoundArtist->setName($lastfmArtist['name'] ?? null);
140
					$unfoundArtist->setMbid($lastfmArtist['mbid'] ?? null);
141
					$unfoundArtist->setLastfmUrl($lastfmArtist['url'] ?? null);
142
					$result[] = $unfoundArtist;
143
				}
144
			}
145
		}
146
147
		return $result;
148
	}
149
150
	/**
151
	 * Get artist tracks from the user's library, sorted by their popularity on Last.fm
152
	 * @param int $maxCount Number of tracks to request from Last.fm. Note that the function may return much
153
	 *						less tracks if the top tracks from Last.fm are not present in the user's library.
154
	 * @return Track[]
155
	 */
156
	public function getTopTracks(string $artistName, string $userId, int $maxCount) : array {
157
		$foundTracks = [];
158
159
		$artist = $this->artistBusinessLayer->findAllByName($artistName, $userId, /*$fuzzy=*/false, /*$limit=*/1)[0] ?? null;
160
161
		if ($artist !== null) {
162
			$lastfmResult = $this->getInfoFromLastFm([
163
				'method' => 'artist.getTopTracks',
164
				'artist' => $artistName,
165
				'limit' => $maxCount
166
			]);
167
			$topTracksOnLastfm = $lastfmResult['toptracks']['track'] ?? null;
168
169
			if ($topTracksOnLastfm !== null) {
170
				$libTracks = $this->trackBusinessLayer->findAllByArtist($artist->getId(), $userId);
171
172
				foreach ($topTracksOnLastfm as $lastfmTrack) {
173
					foreach ($libTracks as $libTrack) {
174
						if (\mb_strtolower($lastfmTrack['name']) === \mb_strtolower($libTrack->getTitle())) {
175
							$foundTracks[] = $libTrack;
176
							break;
177
						}
178
					}
179
				}
180
			}
181
		}
182
183
		return $foundTracks;
184
	}
185
186
	private function getInfoFromLastFm($args) {
187
		if (empty($this->apiKey)) {
188
			return ['api_key_set' => false];
189
		} else {
190
			// append the standard args
191
			$args['api_key'] = $this->apiKey;
192
			$args['format'] = 'json';
193
194
			// remove args with null or empty values
195
			$args = \array_filter($args, [Util::class, 'isNonEmptyString']);
196
197
			// glue arg keys and values together ...
198
			$args = \array_map(function ($key, $value) {
199
				return $key . '=' . \urlencode($value);
200
			}, \array_keys($args), $args);
201
			// ... and form the final query string
202
			$queryString = '?' . \implode('&', $args);
203
204
			list($info, $statusCode, $msg) = self::fetchUrl(self::LASTFM_URL . $queryString);
205
			$statusCode = (int)$statusCode;
206
207
			if ($info === false) {
0 ignored issues
show
introduced by
The condition $info === false is always false.
Loading history...
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
	private static function fetchUrl($url) : array {
223
		$content = \file_get_contents($url);
224
225
		// It's some PHP magic that calling file_get_contents creates and populates
226
		// also a local variable array $http_response_header.
227
		list($version, $status_code, $msg) = explode(' ', $http_response_header[0], 3);
228
229
		return [$content, $status_code, $msg];
230
	}
231
}
232