Passed
Push — master ( 54eb8d...5f3e02 )
by Pauli
04:51
created

MusicApiController::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 31
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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