Completed
Push — master ( 014884...42845c )
by Pauli
17s queued 12s
created

ExternalScrobbler::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 24
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 11
nc 1
nop 11
dl 0
loc 24
rs 9.9
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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 Matthew Wells
10
 * @copyright Matthew Wells 2025
11
 */
12
13
namespace OCA\Music\Service;
14
15
use DateTime;
16
use OCA\Music\AppFramework\Core\Logger;
17
use OCA\Music\BusinessLayer\AlbumBusinessLayer;
18
use OCA\Music\BusinessLayer\TrackBusinessLayer;
19
use OCA\Music\Db\Track;
20
use OCP\IConfig;
21
use OCP\IURLGenerator;
22
use OCP\Security\ICrypto;
23
24
class ExternalScrobbler implements Scrobbler
25
{
26
	private IConfig $config;
27
	private Logger $logger;
28
	private IURLGenerator $urlGenerator;
29
	private TrackBusinessLayer $trackBusinessLayer;
30
	private AlbumBusinessLayer $albumBusinessLayer;
31
	private ICrypto $crypto;
32
	private string $name;
33
	private string $identifier;
34
	private string $endpoint;
35
	private string $tokenRequestUrl;
36
	private ?string $appName;
37
38
	public function __construct(
39
		IConfig $config,
40
		Logger $logger,
41
		IURLGenerator $urlGenerator,
42
		TrackBusinessLayer $trackBusinessLayer,
43
		AlbumBusinessLayer $albumBusinessLayer,
44
		ICrypto $crypto,
45
		string $name,
46
		string $identifier,
47
		string $endpoint,
48
		string $tokenRequestUrl,
49
		?string $appName = 'music'
50
	) {
51
		$this->config = $config;
52
		$this->logger = $logger;
53
		$this->urlGenerator = $urlGenerator;
54
		$this->trackBusinessLayer = $trackBusinessLayer;
55
		$this->albumBusinessLayer = $albumBusinessLayer;
56
		$this->crypto = $crypto;
57
		$this->name = $name;
58
		$this->identifier = $identifier;
59
		$this->endpoint = $endpoint;
60
		$this->tokenRequestUrl = $tokenRequestUrl;
61
		$this->appName = $appName;
62
	}
63
64
	/**
65
	 * @throws \Exception when curl initialization or session key save fails
66
	 * @throws ScrobbleServiceException when auth.getSession call fails
67
	 */
68
	public function generateSession(string $token, string $userId) : void {
69
		$xml = $this->execRequest($this->generateMethodParams('auth.getSession', ['token' => $token]));
70
71
		$status = (string)$xml['status'];
72
		if ($status !== 'ok') {
73
			if ($xml instanceof \SimpleXMLElement) {
0 ignored issues
show
introduced by
$xml is always a sub-type of SimpleXMLElement.
Loading history...
74
				$error = (string)$xml->error;
75
				$code = (int)$xml->code;
76
			} else {
77
				$error = 'Empty response';
78
				$code = 0;
79
			}
80
			throw new ScrobbleServiceException($error, $code);
81
		}
82
		$sessionValue = (string)$xml->session->key;
83
84
		$this->saveApiSession($userId, $sessionValue);
85
	}
86
87
	/**
88
	 * @throws \InvalidArgumentException
89
	 */
90
	public function clearSession(string $userId) : void {
91
		try {
92
			$this->config->deleteUserValue($userId, $this->appName, $this->identifier . '.scrobbleSessionKey');
93
		} catch (\InvalidArgumentException $e) {
94
			$this->logger->error(
95
				'Could not delete user config "' . $this->identifier . '.scrobbleSessionKey". ' . $e->getMessage()
96
			);
97
			throw $e;
98
		}
99
	}
100
101
	public function getApiSession(string $userId) : ?string {
102
		$encryptedKey = $this->config->getUserValue($userId, $this->appName, $this->identifier . '.scrobbleSessionKey');
103
		if (!$encryptedKey) {
104
			return null;
105
		}
106
		$key = $this->crypto->decrypt($encryptedKey, $userId . $this->config->getSystemValue('secret'));
107
		return $key;
108
	}
109
110
	public function getName() : string {
111
		return $this->name;
112
	}
113
114
	public function getIdentifier() : string {
115
		return $this->identifier;
116
	}
117
118
	public function getApiKey() : ?string {
119
		return $this->config->getSystemValue('music.' . $this->identifier . '_api_key', null);
120
	}
121
122
	public function getApiSecret() : ?string {
123
		return $this->config->getSystemValue('music.' . $this->identifier . '_api_secret', null);
124
	}
125
126
	public function recordTrackPlayed(int $trackId, string $userId, ?\DateTime $timeOfPlay = null) : void {
127
		$timeOfPlay = $timeOfPlay ?? new \DateTime();
128
		$sessionKey = $this->getApiSession($userId);
129
		if (!$sessionKey) {
130
			return;
131
		}
132
133
		$timestamp = $timeOfPlay->getTimestamp();
134
		$scrobbleData = [
135
			'sk' => $sessionKey
136
		];
137
138
		/** @var array<Track> $tracks */
139
		$tracks = $this->trackBusinessLayer->findById([$trackId], $userId);
140
		$this->albumBusinessLayer->injectAlbumsToTracks($tracks, $userId);
141
		foreach ($tracks as $i => $track) {
142
			if ($track->getAlbum()) {
143
				$albumArtist = $track->getAlbum()->getAlbumArtistName();
144
				if ($albumArtist !== $track->getArtistName()) {
145
					$scrobbleData["albumArtist[{$i}]"] = $albumArtist;
146
				}
147
			}
148
			$scrobbleData["artist[{$i}]"] = $track->getArtistName();
149
			$scrobbleData["track[{$i}]"] = $track->getTitle();
150
			$scrobbleData["timestamp[{$i}]"] = $timestamp;
151
			$scrobbleData["album[{$i}]"] = $track->getAlbumName();
152
			$scrobbleData["trackNumber[{$i}]"] = $track->getNumber();
153
		}
154
		$xml = $this->execRequest($this->generateMethodParams('track.scrobble', $scrobbleData));
155
156
		if ((string)$xml['status'] !== 'ok') {
157
			$this->logger->warning('Failed to scrobble to ' . $this->name);
158
		}
159
	}
160
161
	public function getTokenRequestUrl(): ?string {
162
		$apiKey = $this->getApiKey();
163
		if (!$apiKey) {
164
			return null;
165
		}
166
167
		$tokenHandleUrl = $this->urlGenerator->linkToRouteAbsolute('music.scrobbler.handleToken', [
168
			'serviceIdentifier' => $this->identifier
169
		]);
170
		return "{$this->tokenRequestUrl}?api_key={$apiKey}&cb={$tokenHandleUrl}";
171
	}
172
173
	private function saveApiSession(string $userId, string $sessionValue) : void {
174
		try {
175
			$encryptedKey = $this->crypto->encrypt(
176
				$sessionValue,
177
				$userId . $this->config->getSystemValue('secret')
178
			);
179
			$this->config->setUserValue($userId, $this->appName, $this->identifier . '.scrobbleSessionKey', $encryptedKey);
180
		} catch (\Exception $e) {
181
			$this->logger->error('Encryption of scrobble session key failed');
182
			throw $e;
183
		}
184
	}
185
186
	/**
187
	 * @param array<string, string|array> $params
188
	 */
189
	private function generateSignature(array $params) : string {
190
		\ksort($params);
191
		$paramString = '';
192
		foreach ($params as $key => $value) {
193
			if (\is_array($value)) {
194
				foreach ($value as $valIdx => $valVal) {
195
					$paramString .= "{$key}[{$valIdx}]{$valVal}";
196
				}
197
			} else {
198
				$paramString .= $key . $value;
199
			}
200
		}
201
202
		$paramString .= $this->getApiSecret();
203
		return \md5($paramString);
204
	}
205
206
	/**
207
	 * @return array<string, string>
208
	 * @param array<string, mixed> $moreParams
209
	 * @param bool $sign
210
	 * @return array<string, mixed>
211
	 */
212
	private function generateMethodParams(string $method, array $moreParams = [], bool $sign = true) : array {
213
		$params = \array_merge($moreParams, [
214
			'method' => $method,
215
			'api_key' => $this->getApiKey()
216
		]);
217
218
		if ($sign) {
219
			$params['api_sig'] = $this->generateSignature($params);
220
		}
221
222
		return $params;
223
	}
224
225
	private function execRequest(array $params) : ?\SimpleXMLElement {
226
		$ch = \curl_init();
227
		if (!$ch) {
228
			$this->logger->error('Failed to initialize a curl handle, is the php curl extension installed?');
229
			throw new \RuntimeException('Unable to initialize a curl handle');
230
		}
231
		\curl_setopt($ch, \CURLOPT_URL, $this->endpoint);
232
		\curl_setopt($ch, \CURLOPT_CONNECTTIMEOUT, 10);
233
		\curl_setopt($ch, \CURLOPT_POST, true);
234
		\curl_setopt($ch, \CURLOPT_RETURNTRANSFER, true);
235
		\curl_setopt($ch, \CURLOPT_POSTFIELDS, \http_build_query($params));
236
		/** @var string $xmlString */
237
		$xmlString = \curl_exec($ch) ?: '';
238
		/** @var \SimpleXMLElement|false $xml */
239
		$xml = \simplexml_load_string($xmlString);
240
		return $xml ?: null;
0 ignored issues
show
introduced by
$xml is of type SimpleXMLElement, thus it always evaluated to true.
Loading history...
241
	}
242
}
243