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

MusicApiController   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 346
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 152
dl 0
loc 346
rs 8.96
c 3
b 0
f 0
wmc 43

20 Methods

Rating   Name   Duplication   Size   Complexity  
A removeScanned() 0 4 1
A download() 0 8 2
A fileInfo() 0 7 2
A __construct() 0 31 1
A fileDetails() 0 15 3
A resetScanned() 0 3 1
A folders() 0 4 1
A filePath() 0 9 2
A getScanState() 0 2 1
A collection() 0 15 3
A prepareCollection() 0 13 2
A genres() 0 6 1
A scan() 0 18 2
A trackByFileId() 0 6 2
A scrobble() 0 6 2
A similarArtists() 0 10 2
A artistDetails() 0 6 2
A fileLyrics() 0 14 4
A albumDetails() 0 17 6
A findDetails() 0 5 3

How to fix   Complexity   

Complex Class

Complex classes like MusicApiController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MusicApiController, and based on these observations, apply Extract Interface, too.

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