Passed
Pull Request — master (#1266)
by Matthew
04:13
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
			throw new ScrobbleServiceException((string)$xml->error, (int)$xml->error['code']);
74
		}
75
		$sessionValue = (string)$xml->session->key;
76
77
		$this->saveApiSession($userId, $sessionValue);
78
	}
79
80
	/**
81
	 * @throws \InvalidArgumentException
82
	 */
83
	public function clearSession(?string $userId) : void {
84
		try {
85
			$this->config->deleteUserValue($userId, $this->appName, $this->identifier . '.scrobbleSessionKey');
86
		} catch (\InvalidArgumentException $e) {
87
			$this->logger->error(
88
				'Could not delete user config "' . $this->identifier . '.scrobbleSessionKey". ' . $e->getMessage()
89
			);
90
			throw $e;
91
		}
92
	}
93
94
	public function getApiSession(string $userId) : ?string {
95
		$encryptedKey = $this->config->getUserValue($userId, $this->appName, $this->identifier . '.scrobbleSessionKey');
96
		if (!$encryptedKey) {
97
			return null;
98
		}
99
		$key = $this->crypto->decrypt($encryptedKey, $userId . $this->config->getSystemValue('secret'));
100
		return $key;
101
	}
102
103
	public function getName() : string {
104
		return $this->name;
105
	}
106
107
	public function getIdentifier() : string {
108
		return $this->identifier;
109
	}
110
111
	public function getApiKey() : ?string {
112
		return $this->config->getSystemValue('music.' . $this->identifier . '_api_key', null);
113
	}
114
115
	public function getApiSecret() : ?string {
116
		return $this->config->getSystemValue('music.' . $this->identifier . '_api_secret', null);
117
	}
118
119
	public function recordTrackPlayed(int $trackId, string $userId, ?\DateTime $timeOfPlay = null) : void {
120
		$timeOfPlay = $timeOfPlay ?? new \DateTime();
121
		$sessionKey = $this->getApiSession($userId);
122
		if (!$sessionKey) {
123
			return;
124
		}
125
126
		$timestamp = $timeOfPlay->getTimestamp();
127
		$scrobbleData = [
128
			'sk' => $sessionKey
129
		];
130
131
		/** @var array<Track> $tracks */
132
		$tracks = $this->trackBusinessLayer->findById([$trackId], $userId);
133
		$this->albumBusinessLayer->injectAlbumsToTracks($tracks, $userId);
134
		foreach ($tracks as $i => $track) {
135
			if ($track->getAlbum()) {
136
				$albumArtist = $track->getAlbum()->getAlbumArtistName();
137
				if ($albumArtist !== $track->getArtistName()) {
138
					$scrobbleData["albumArtist[{$i}]"] = $albumArtist;
139
				}
140
			}
141
			$scrobbleData["artist[{$i}]"] = $track->getArtistName();
142
			$scrobbleData["track[{$i}]"] = $track->getTitle();
143
			$scrobbleData["timestamp[{$i}]"] = $timestamp;
144
			$scrobbleData["album[{$i}]"] = $track->getAlbumName();
145
			$scrobbleData["trackNumber[{$i}]"] = $track->getNumber();
146
		}
147
        $xml = $this->execRequest($this->generateMethodParams('track.scrobble', $scrobbleData));
148
149
		if ((string)$xml['status'] !== 'ok') {
150
			$this->logger->warning('Failed to scrobble to ' . $this->name);
151
		}
152
	}
153
154
	public function getTokenRequestUrl(): ?string {
155
		$apiKey = $this->getApiKey();
156
		if (!$apiKey) {
157
			return null;
158
		}
159
160
		$tokenHandleUrl = $this->urlGenerator->linkToRouteAbsolute('music.scrobbler.handleToken', [
161
			'serviceIdentifier' => $this->identifier
162
		]);
163
		return "{$this->tokenRequestUrl}?api_key={$apiKey}&cb={$tokenHandleUrl}";
164
	}
165
166
	private function saveApiSession(string $userId, string $sessionValue) : void {
167
		try {
168
			$encryptedKey = $this->crypto->encrypt(
169
				$sessionValue,
170
				$userId . $this->config->getSystemValue('secret')
171
			);
172
			$this->config->setUserValue($userId, $this->appName, $this->identifier . '.scrobbleSessionKey', $encryptedKey);
173
		} catch (\Exception $e) {
174
			$this->logger->error('Encryption of scrobble session key failed');
175
			throw $e;
176
		}
177
	}
178
179
	/**
180
	 * @param array<string, string|array> $params
181
	 */
182
	private function generateSignature(array $params) : string {
183
		\ksort($params);
184
		$paramString = '';
185
		foreach ($params as $key => $value) {
186
			if (\is_array($value)) {
187
				foreach ($value as $valIdx => $valVal) {
188
					$paramString .= "{$key}[{$valIdx}]{$valVal}";
189
				}
190
			} else {
191
				$paramString .= $key . $value;
192
			}
193
		}
194
195
		$paramString .= $this->getApiSecret();
196
		return \md5($paramString);
197
	}
198
199
	/**
200
	 * @return array<string, string>
201
	 * @param array<string, mixed> $moreParams
202
	 * @param bool $sign
203
	 * @return array<string, mixed>
204
	 */
205
	private function generateMethodParams(string $method, array $moreParams = [], bool $sign = true) : array {
206
		$params = \array_merge($moreParams, [
207
			'method' => $method,
208
			'api_key' => $this->getApiKey()
209
		]);
210
211
		if ($sign) {
212
			$params['api_sig'] = $this->generateSignature($params);
213
		}
214
215
		return $params;
216
	}
217
218
    private function execRequest(array $params) : ?\SimpleXMLElement {
219
		$ch = \curl_init();
220
		if (!$ch) {
221
			$this->logger->error('Failed to initialize a curl handle, is the php curl extension installed?');
222
			throw new \RuntimeException('Unable to initialize a curl handle');
223
		}
224
		\curl_setopt($ch, \CURLOPT_URL, $this->endpoint);
225
		\curl_setopt($ch, \CURLOPT_CONNECTTIMEOUT, 10);
226
		\curl_setopt($ch, \CURLOPT_POST, true);
227
		\curl_setopt($ch, \CURLOPT_RETURNTRANSFER, true);
228
        \curl_setopt($ch, \CURLOPT_POSTFIELDS, \http_build_query($params));
229
		$xmlString = \curl_exec($ch);
230
        \assert(\is_string($xmlString));
231
		$xml = \simplexml_load_string($xmlString);
232
        return $xml ?: null;
0 ignored issues
show
introduced by
$xml is of type SimpleXMLElement, thus it always evaluated to true.
Loading history...
233
    }
234
}
235