Passed
Push — master ( 041459...b27899 )
by Pauli
03:55
created

MusicApiController::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 25
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 12
nc 1
nop 13
dl 0
loc 25
rs 9.8666
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 Morris Jobke <[email protected]>
10
 * @author Pauli Järvinen <[email protected]>
11
 * @copyright Morris Jobke 2013, 2014
12
 * @copyright Pauli Järvinen 2017 - 2025
13
 */
14
15
namespace OCA\Music\Controller;
16
17
use OCP\AppFramework\Controller;
18
use OCP\AppFramework\Http;
19
use OCP\AppFramework\Http\DataDisplayResponse;
20
use OCP\AppFramework\Http\JSONResponse;
21
use OCP\AppFramework\Http\Response;
22
use OCP\IRequest;
23
24
use OCA\Music\AppFramework\BusinessLayer\BusinessLayerException;
25
use OCA\Music\AppFramework\Core\Logger;
26
use OCA\Music\BusinessLayer\GenreBusinessLayer;
27
use OCA\Music\BusinessLayer\TrackBusinessLayer;
28
use OCA\Music\Db\Maintenance;
29
use OCA\Music\Http\ErrorResponse;
30
use OCA\Music\Http\FileStreamResponse;
31
use OCA\Music\Service\CollectionService;
32
use OCA\Music\Service\CoverService;
33
use OCA\Music\Service\DetailsService;
34
use OCA\Music\Service\LastfmService;
35
use OCA\Music\Service\LibrarySettings;
36
use OCA\Music\Service\Scanner;
37
use OCA\Music\Utility\HttpUtil;
38
use OCA\Music\Utility\Util;
39
40
class MusicApiController extends Controller {
41
42
	private TrackBusinessLayer $trackBusinessLayer;
43
	private GenreBusinessLayer $genreBusinessLayer;
44
	private Scanner $scanner;
45
	private CollectionService $collectionService;
46
	private CoverService $coverService;
47
	private DetailsService $detailsService;
48
	private LastfmService $lastfmService;
49
	private Maintenance $maintenance;
50
	private LibrarySettings $librarySettings;
51
	private string $userId;
52
	private Logger $logger;
53
54
	public function __construct(string $appName,
55
								IRequest $request,
56
								TrackBusinessLayer $trackBusinessLayer,
57
								GenreBusinessLayer $genreBusinessLayer,
58
								Scanner $scanner,
59
								CollectionService $collectionService,
60
								CoverService $coverService,
61
								DetailsService $detailsService,
62
								LastfmService $lastfmService,
63
								Maintenance $maintenance,
64
								LibrarySettings $librarySettings,
65
								?string $userId,
66
								Logger $logger) {
67
		parent::__construct($appName, $request);
68
		$this->trackBusinessLayer = $trackBusinessLayer;
69
		$this->genreBusinessLayer = $genreBusinessLayer;
70
		$this->scanner = $scanner;
71
		$this->collectionService = $collectionService;
72
		$this->coverService = $coverService;
73
		$this->detailsService = $detailsService;
74
		$this->lastfmService = $lastfmService;
75
		$this->maintenance = $maintenance;
76
		$this->librarySettings = $librarySettings;
77
		$this->userId = $userId ?? ''; // null case should happen only when the user has already logged out
78
		$this->logger = $logger;
79
	}
80
81
	/**
82
	 * @NoAdminRequired
83
	 * @NoCSRFRequired
84
	 */
85
	public function prepareCollection() : JSONResponse {
86
		$hash = $this->collectionService->getCachedJsonHash();
87
		if ($hash === null) {
88
			// build the collection but ignore the data for now
89
			$this->collectionService->getJson();
90
			$hash = $this->collectionService->getCachedJsonHash();
91
		}
92
		$coverToken = $this->coverService->createAccessToken($this->userId);
93
94
		return new JSONResponse([
95
			'hash' => $hash,
96
			'cover_token' => $coverToken,
97
			'ignored_articles' => $this->librarySettings->getIgnoredArticles($this->userId)
98
		]);
99
	}
100
101
	/**
102
	 * @NoAdminRequired
103
	 * @NoCSRFRequired
104
	 */
105
	public function collection() : DataDisplayResponse {
106
		$collectionJson = $this->collectionService->getJson();
107
		$response = new DataDisplayResponse($collectionJson);
108
		$response->addHeader('Content-Type', 'application/json; charset=utf-8');
109
110
		// Instruct the client to cache the result in case it requested the collection with
111
		// the correct hash. The hash could be incorrect if the collection would have changed
112
		// between calls to prepareCollection() and collection().
113
		$requestHash = $this->request->getParam('hash');
114
		$actualHash = $this->collectionService->getCachedJsonHash();
115
		if (!empty($actualHash) && $requestHash === $actualHash) {
116
			HttpUtil::setClientCachingDays($response, 90);
117
		}
118
119
		return $response;
120
	}
121
122
	/**
123
	 * @NoAdminRequired
124
	 * @NoCSRFRequired
125
	 */
126
	public function folders() : JSONResponse {
127
		$musicFolder = $this->librarySettings->getFolder($this->userId);
128
		$folders = $this->trackBusinessLayer->findAllFolders($this->userId, $musicFolder);
129
		return new JSONResponse($folders);
130
	}
131
132
	/**
133
	 * @NoAdminRequired
134
	 * @NoCSRFRequired
135
	 */
136
	public function genres() : JSONResponse {
137
		$genres = $this->genreBusinessLayer->findAllWithTrackIds($this->userId);
138
		$unscanned =  $this->trackBusinessLayer->findFilesWithoutScannedGenre($this->userId);
139
		return new JSONResponse([
140
			'genres' => \array_map(fn($g) => $g->toApi(), $genres),
141
			'unscanned' => $unscanned
142
		]);
143
	}
144
145
	/**
146
	 * @NoAdminRequired
147
	 * @NoCSRFRequired
148
	 */
149
	public function trackByFileId(int $fileId) : JSONResponse {
150
		$track = $this->trackBusinessLayer->findByFileId($fileId, $this->userId);
151
		if ($track !== null) {
152
			return new JSONResponse($track->toCollection());
153
		} else {
154
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
155
		}
156
	}
157
158
	/**
159
	 * @NoAdminRequired
160
	 * @NoCSRFRequired
161
	 */
162
	public function getScanState() : JSONResponse {
163
		return new JSONResponse($this->scanner->getStatusOfLibraryFiles($this->userId));
164
	}
165
166
	/**
167
	 * @param string|int|bool|null $finalize
168
	 *
169
	 * @NoAdminRequired
170
	 * @NoCSRFRequired
171
	 * @UseSession to keep the session reserved while execution in progress
172
	 */
173
	public function scan(string $files, /*mixed*/ $finalize) : JSONResponse {
174
		// extract the parameters
175
		$fileIds = \array_map('intval', \explode(',', $files));
176
		$finalize = \filter_var($finalize, FILTER_VALIDATE_BOOLEAN);
177
178
		list('count' => $filesScanned) = $this->scanner->scanFiles($this->userId, $fileIds);
179
180
		$albumCoversUpdated = false;
181
		if ($finalize) {
182
			$albumCoversUpdated = $this->scanner->findAlbumCovers($this->userId);
183
			$this->scanner->findArtistCovers($this->userId);
184
			$totalCount = $this->trackBusinessLayer->count($this->userId);
185
			$this->logger->info("Scanning finished, user $this->userId has $totalCount scanned tracks in total");
186
		}
187
188
		return new JSONResponse([
189
			'filesScanned' => $filesScanned,
190
			'albumCoversUpdated' => $albumCoversUpdated
191
		]);
192
	}
193
194
	/**
195
	 * @NoAdminRequired
196
	 * @NoCSRFRequired
197
	 * @UseSession to keep the session reserved while execution in progress
198
	 */
199
	public function removeScanned(string $files) : JSONResponse {
200
		$fileIds = \array_map('intval', \explode(',', $files));
201
		$anythingRemoved = $this->scanner->deleteAudio($fileIds, [$this->userId]);
202
		return new JSONResponse(['filesRemoved' => $anythingRemoved]);
203
	}
204
205
	/**
206
	 * @NoAdminRequired
207
	 * @NoCSRFRequired
208
	 * @UseSession to keep the session reserved while execution in progress
209
	 */
210
	public function resetScanned() : JSONResponse {
211
		$this->maintenance->resetLibrary($this->userId);
212
		return new JSONResponse(['success' => true]);
213
	}
214
215
	/**
216
	 * @NoAdminRequired
217
	 * @NoCSRFRequired
218
	 */
219
	public function download(int $fileId) : Response {
220
		$nodes = $this->scanner->resolveUserFolder($this->userId)->getById($fileId);
221
		$node = $nodes[0] ?? null;
222
		if ($node instanceof \OCP\Files\File) {
223
			return new FileStreamResponse($node);
224
		}
225
226
		return new ErrorResponse(Http::STATUS_NOT_FOUND, 'file not found');
227
	}
228
229
	/**
230
	 * @NoAdminRequired
231
	 * @NoCSRFRequired
232
	 */
233
	public function filePath(int $fileId) : JSONResponse {
234
		$userFolder = $this->scanner->resolveUserFolder($this->userId);
235
		$nodes = $userFolder->getById($fileId);
236
		if (\count($nodes) == 0) {
237
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
238
		} else {
239
			$node = $nodes[0];
240
			$path = $userFolder->getRelativePath($node->getPath());
241
			return new JSONResponse(['path' => Util::urlEncodePath($path)]);
242
		}
243
	}
244
245
	/**
246
	 * @NoAdminRequired
247
	 * @NoCSRFRequired
248
	 */
249
	public function fileInfo(int $fileId) : JSONResponse {
250
		$userFolder = $this->scanner->resolveUserFolder($this->userId);
251
		$info = $this->scanner->getFileInfo($fileId, $this->userId, $userFolder);
252
		if ($info) {
253
			return new JSONResponse($info);
254
		} else {
255
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
256
		}
257
	}
258
259
	/**
260
	 * @NoAdminRequired
261
	 * @NoCSRFRequired
262
	 */
263
	public function fileDetails(int $fileId) : JSONResponse {
264
		$userFolder = $this->scanner->resolveUserFolder($this->userId);
265
		$details = $this->detailsService->getDetails($fileId, $userFolder);
266
		if ($details) {
267
			// metadata extracted, attempt to include also the data from Last.fm
268
			$track = $this->trackBusinessLayer->findByFileId($fileId, $this->userId);
269
			if ($track) {
270
				$details['lastfm'] = $this->lastfmService->getTrackInfo($track->getId(), $this->userId);
271
			} else {
272
				$this->logger->warning("Track with file ID $fileId was not found => can't fetch info from Last.fm");
273
			}
274
275
			return new JSONResponse($details);
276
		} else {
277
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
278
		}
279
	}
280
281
	/**
282
	 * @NoAdminRequired
283
	 * @NoCSRFRequired
284
	 */
285
	public function fileLyrics(int $fileId, ?string $format) : Response {
286
		$userFolder = $this->scanner->resolveUserFolder($this->userId);
287
		if ($format == 'plaintext') {
288
			$lyrics = $this->detailsService->getLyricsAsPlainText($fileId, $userFolder);
289
			if (!empty($lyrics)) {
290
				return new DataDisplayResponse($lyrics, Http::STATUS_OK, ['Content-Type' => 'text/plain; charset=utf-8']);
291
			}
292
		} else {
293
			$lyrics = $this->detailsService->getLyricsAsStructured($fileId, $userFolder);
294
			if (!empty($lyrics)) {
295
				return new JSONResponse($lyrics);
296
			}
297
		}
298
		return new ErrorResponse(Http::STATUS_NOT_FOUND);
299
	}
300
301
	/**
302
	 * @NoAdminRequired
303
	 * @NoCSRFRequired
304
	 */
305
	public function scrobble(int $trackId) : JSONResponse {
306
		try {
307
			$this->trackBusinessLayer->recordTrackPlayed($trackId, $this->userId);
308
			return new JSONResponse(['success' => true]);
309
		} catch (BusinessLayerException $e) {
310
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
311
		}
312
	}
313
314
	/**
315
	 * @param string|int|bool|null $embedCoverArt
316
	 * @NoAdminRequired
317
	 * @NoCSRFRequired
318
	 */
319
	public function albumDetails(int $albumId, /*mixed*/ $embedCoverArt=false) : JSONResponse {
320
		$embedCoverArt = \filter_var($embedCoverArt, FILTER_VALIDATE_BOOLEAN);
321
		try {
322
			$info = $this->lastfmService->getAlbumInfo($albumId, $this->userId);
323
			if ($embedCoverArt && isset($info['album']['image'])) {
324
				$lastImage = \end($info['album']['image']);
325
				$imageSrc = $lastImage['#text'] ?? null;
326
				if (\is_string($imageSrc)) {
327
					$image = HttpUtil::loadFromUrl($imageSrc);
328
					if ($image['content']) {
329
						$info['album']['imageData'] = 'data:' . $image['content_type'] . ';base64,' . \base64_encode($image['content']);
330
					}
331
				}
332
			}
333
			return new JSONResponse($info);
334
		} catch (BusinessLayerException $e) {
335
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
336
		}
337
	}
338
339
	/**
340
	 * @NoAdminRequired
341
	 * @NoCSRFRequired
342
	 */
343
	public function artistDetails(int $artistId) : JSONResponse {
344
		try {
345
			$info = $this->lastfmService->getArtistInfo($artistId, $this->userId);
346
			return new JSONResponse($info);
347
		} catch (BusinessLayerException $e) {
348
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
349
		}
350
	}
351
352
	/**
353
	 * @NoAdminRequired
354
	 * @NoCSRFRequired
355
	 */
356
	public function similarArtists(int $artistId) : JSONResponse {
357
		try {
358
			$similar = $this->lastfmService->getSimilarArtists($artistId, $this->userId, /*includeNotPresent=*/true);
359
			return new JSONResponse(\array_map(fn($artist) => [
360
				'id' => $artist->getId(),
361
				'name' => $artist->getName(),
362
				'url' => $artist->getLastfmUrl()
363
			], $similar));
364
		} catch (BusinessLayerException $e) {
365
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
366
		}
367
	}
368
}
369