Passed
Push — master ( 23afda...b1ae99 )
by Pauli
03:08
created

MusicApiController::setClientCaching()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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