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

MusicApiController   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 326
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 144
dl 0
loc 326
rs 9.2
c 3
b 0
f 0
wmc 40

19 Methods

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

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 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