Passed
Pull Request — master (#1266)
by Matthew
04:57
created

ScrobblerService::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 24
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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

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 ScrobblerService
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
		$ch = $this->makeCurlHandle();
70
		\curl_setopt($ch, \CURLOPT_POSTFIELDS, \http_build_query(
71
			$this->generateMethodParams('auth.getSession', ['token' => $token])
72
		));
73
		$xml = \simplexml_load_string(\curl_exec($ch));
0 ignored issues
show
Bug introduced by
It seems like curl_exec($ch) can also be of type true; however, parameter $data of simplexml_load_string() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

73
		$xml = \simplexml_load_string(/** @scrutinizer ignore-type */ \curl_exec($ch));
Loading history...
74
75
		$status = (string)$xml['status'];
76
		if ($status !== 'ok') {
77
			throw new ScrobbleServiceException((string)$xml->error, (int)$xml->error['code']);
78
		}
79
		$sessionValue = (string)$xml->session->key;
80
81
		$this->saveApiSession($userId, $sessionValue);
82
	}
83
84
	/**
85
	 * @throws \InvalidArgumentException
86
	 */
87
	public function clearSession(?string $userId) : void {
88
		try {
89
			$this->config->deleteUserValue($userId, $this->appName, $this->identifier . '.scrobbleSessionKey');
90
		} catch (\InvalidArgumentException $e) {
91
			$this->logger->error(
92
				'Could not delete user config "' . $this->identifier . '.scrobbleSessionKey". ' . $e->getMessage()
93
			);
94
			throw $e;
95
		}
96
	}
97
98
	public function getApiSession(string $userId) : ?string {
99
		$encryptedKey = $this->config->getUserValue($userId, $this->appName, $this->identifier . '.scrobbleSessionKey');
100
		if (!$encryptedKey) {
101
			return null;
102
		}
103
		$key = $this->crypto->decrypt($encryptedKey, $userId . $this->config->getSystemValue('secret'));
104
		return $key;
105
	}
106
107
	public function getName() : string {
108
		return $this->name;
109
	}
110
111
	public function getIdentifier() : string {
112
		return $this->identifier;
113
	}
114
115
	public function getApiKey() : ?string {
116
		return $this->config->getSystemValue('music.' . $this->identifier . '_api_key', null);
117
	}
118
119
	public function getApiSecret() : ?string {
120
		return $this->config->getSystemValue('music.' . $this->identifier . '_api_secret', null);
121
	}
122
123
	/**
124
	 * @param array<int,mixed> $trackIds
125
	 */
126
	public function scrobbleTrack(array $trackIds, string $userId, \DateTime $timeOfPlay) : void {
127
		$sessionKey = $this->getApiSession($userId);
128
		if (!$sessionKey) {
129
			return;
130
		}
131
132
		$timestamp = $timeOfPlay->getTimestamp();
133
		$scrobbleData = [
134
			'sk' => $sessionKey
135
		];
136
137
		/** @var array<Track> $tracks */
138
		$tracks = $this->trackBusinessLayer->findById($trackIds, $userId);
139
		$this->albumBusinessLayer->injectAlbumsToTracks($tracks, $userId);
140
		foreach ($tracks as $i => $track) {
141
			if ($track->getAlbum()) {
142
				$albumArtist = $track->getAlbum()->getAlbumArtistName();
143
				if ($albumArtist !== $track->getArtistName()) {
144
					$scrobbleData["albumArtist[{$i}]"] = $albumArtist;
145
				}
146
			}
147
			$scrobbleData["artist[{$i}]"] = $track->getArtistName();
148
			$scrobbleData["track[{$i}]"] = $track->getTitle();
149
			$scrobbleData["timestamp[{$i}]"] = $timestamp;
150
			$scrobbleData["album[{$i}]"] = $track->getAlbumName();
151
			$scrobbleData["trackNumber[{$i}]"] = $track->getNumber();
152
		}
153
		$scrobbleData = $this->generateMethodParams('track.scrobble', $scrobbleData);
154
155
		$ch = $this->makeCurlHandle();
156
		\curl_setopt($ch, \CURLOPT_POSTFIELDS, \http_build_query($scrobbleData));
157
		$xml = \simplexml_load_string(\curl_exec($ch));
0 ignored issues
show
Bug introduced by
It seems like curl_exec($ch) can also be of type true; however, parameter $data of simplexml_load_string() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

157
		$xml = \simplexml_load_string(/** @scrutinizer ignore-type */ \curl_exec($ch));
Loading history...
158
159
		if ((string)$xml['status'] !== 'ok') {
160
			$this->logger->warning('Failed to scrobble to ' . $this->name);
161
		}
162
	}
163
164
	public function getTokenRequestUrl(): ?string {
165
		$apiKey = $this->getApiKey();
166
		if (!$apiKey) {
167
			return null;
168
		}
169
170
		$tokenHandleUrl = $this->urlGenerator->linkToRouteAbsolute('music.scrobbler.handleToken', [
171
			'serviceIdentifier' => $this->identifier
172
		]);
173
		return "{$this->tokenRequestUrl}?api_key={$apiKey}&cb={$tokenHandleUrl}";
174
	}
175
176
	private function saveApiSession(string $userId, string $sessionValue) : void {
177
		try {
178
			$encryptedKey = $this->crypto->encrypt(
179
				$sessionValue,
180
				$userId . $this->config->getSystemValue('secret')
181
			);
182
			$this->config->setUserValue($userId, $this->appName, $this->identifier . '.scrobbleSessionKey', $encryptedKey);
183
		} catch (\Exception $e) {
184
			$this->logger->error('Encryption of scrobble session key failed');
185
			throw $e;
186
		}
187
	}
188
189
	/**
190
	 * @param array<string, string|array> $params
191
	 */
192
	private function generateSignature(array $params) : string {
193
		\ksort($params);
194
		$paramString = '';
195
		foreach ($params as $key => $value) {
196
			if (\is_array($value)) {
197
				foreach ($value as $valIdx => $valVal) {
198
					$paramString .= "{$key}[{$valIdx}]{$valVal}";
199
				}
200
			} else {
201
				$paramString .= $key . $value;
202
			}
203
		}
204
205
		$paramString .= $apiSecret = $this->getApiSecret();
0 ignored issues
show
Unused Code introduced by
The assignment to $apiSecret is dead and can be removed.
Loading history...
206
		return \md5($paramString);
207
	}
208
209
	/**
210
	 * @return array<string, string>
211
	 * @param array<string, mixed> $moreParams
212
	 * @param bool $sign
213
	 * @return array<string, mixed>
214
	 */
215
	private function generateMethodParams(string $method, array $moreParams = [], bool $sign = true) : array {
216
		$params = \array_merge($moreParams, [
217
			'method' => $method,
218
			'api_key' => $this->getApiKey()
219
		]);
220
221
		if ($sign) {
222
			$params['api_sig'] = $this->generateSignature($params);
223
		}
224
225
		return $params;
226
	}
227
228
	/**
229
	 * @return resource (in PHP8+ return \CurlHandle)
230
	 * @throws \RuntimeException when unable to initialize a cURL handle
231
	 */
232
	private function makeCurlHandle() {
233
		$ch = \curl_init();
234
		if (!$ch) {
235
			$this->logger->error('Failed to initialize a curl handle, is the php curl extension installed?');
236
			throw new \RuntimeException('Unable to initialize a curl handle');
237
		}
238
		\curl_setopt($ch, \CURLOPT_URL, $this->endpoint);
239
		\curl_setopt($ch, \CURLOPT_CONNECTTIMEOUT, 10);
240
		\curl_setopt($ch, \CURLOPT_POST, true);
241
		\curl_setopt($ch, \CURLOPT_RETURNTRANSFER, true);
242
		return $ch;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $ch also could return the type CurlHandle which is incompatible with the documented return type resource.
Loading history...
243
	}
244
}
245