Passed
Push — master ( ce9a66...c3a2b5 )
by Pauli
02:52
created

ApiController::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 - 2024
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\Files\Folder;
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\Utility\CollectionHelper;
33
use OCA\Music\Utility\CoverHelper;
34
use OCA\Music\Utility\DetailsHelper;
35
use OCA\Music\Utility\LastfmService;
36
use OCA\Music\Utility\LibrarySettings;
37
use OCA\Music\Utility\Scanner;
38
use OCA\Music\Utility\Util;
39
40
class ApiController extends Controller {
41
42
	private TrackBusinessLayer $trackBusinessLayer;
43
	private GenreBusinessLayer $genreBusinessLayer;
44
	private Scanner $scanner;
45
	private CollectionHelper $collectionHelper;
46
	private CoverHelper $coverHelper;
47
	private DetailsHelper $detailsHelper;
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
								CollectionHelper $collectionHelper,
60
								CoverHelper $coverHelper,
61
								DetailsHelper $detailsHelper,
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->collectionHelper = $collectionHelper;
72
		$this->coverHelper = $coverHelper;
73
		$this->detailsHelper = $detailsHelper;
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() {
86
		$hash = $this->collectionHelper->getCachedJsonHash();
87
		if ($hash === null) {
88
			// build the collection but ignore the data for now
89
			$this->collectionHelper->getJson();
90
			$hash = $this->collectionHelper->getCachedJsonHash();
91
		}
92
		$coverToken = $this->coverHelper->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() {
106
		$collectionJson = $this->collectionHelper->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->collectionHelper->getCachedJsonHash();
115
		if (!empty($actualHash) && $requestHash === $actualHash) {
116
			self::setClientCaching($response, 90); // cache for 3 months
117
		}
118
119
		return $response;
120
	}
121
122
	/**
123
	 * @NoAdminRequired
124
	 * @NoCSRFRequired
125
	 */
126
	public function folders() {
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() {
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) {
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() {
163
		return new JSONResponse([
164
			'unscannedFiles' => $this->scanner->getUnscannedMusicFileIds($this->userId),
165
			'dirtyFiles' => $this->scanner->getDirtyMusicFileIds($this->userId),
166
			'scannedCount' => $this->trackBusinessLayer->count($this->userId)
167
		]);
168
	}
169
170
	/**
171
	 * @NoAdminRequired
172
	 * @NoCSRFRequired
173
	 * @UseSession to keep the session reserved while execution in progress
174
	 */
175
	public function scan(string $files, $finalize) {
176
		// extract the parameters
177
		$fileIds = \array_map('intval', \explode(',', $files));
178
		$finalize = \filter_var($finalize, FILTER_VALIDATE_BOOLEAN);
179
180
		$filesScanned = $this->scanner->scanFiles($this->userId, $fileIds);
181
182
		$albumCoversUpdated = false;
183
		if ($finalize) {
184
			$albumCoversUpdated = $this->scanner->findAlbumCovers($this->userId);
185
			$this->scanner->findArtistCovers($this->userId);
186
			$totalCount = $this->trackBusinessLayer->count($this->userId);
187
			$this->logger->log("Scanning finished, user $this->userId has $totalCount scanned tracks in total", 'info');
188
		}
189
190
		return new JSONResponse([
191
			'filesScanned' => $filesScanned,
192
			'albumCoversUpdated' => $albumCoversUpdated
193
		]);
194
	}
195
196
	/**
197
	 * @NoAdminRequired
198
	 * @NoCSRFRequired
199
	 * @UseSession to keep the session reserved while execution in progress
200
	 */
201
	public function resetScanned() {
202
		$this->maintenance->resetLibrary($this->userId);
203
		return new JSONResponse(['success' => true]);
204
	}
205
206
	/**
207
	 * @NoAdminRequired
208
	 * @NoCSRFRequired
209
	 */
210
	public function download(int $fileId) {
211
		$nodes = $this->scanner->resolveUserFolder($this->userId)->getById($fileId);
212
		$node = $nodes[0] ?? null;
213
		if ($node instanceof \OCP\Files\File) {
214
			return new FileStreamResponse($node);
215
		}
216
217
		return new ErrorResponse(Http::STATUS_NOT_FOUND, 'file not found');
218
	}
219
220
	/**
221
	 * @NoAdminRequired
222
	 * @NoCSRFRequired
223
	 */
224
	public function filePath(int $fileId) {
225
		$userFolder = $this->scanner->resolveUserFolder($this->userId);
226
		$nodes = $userFolder->getById($fileId);
227
		if (\count($nodes) == 0) {
228
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
229
		} else {
230
			$node = $nodes[0];
231
			$path = $userFolder->getRelativePath($node->getPath());
232
			return new JSONResponse(['path' => Util::urlEncodePath($path)]);
233
		}
234
	}
235
236
	/**
237
	 * @NoAdminRequired
238
	 * @NoCSRFRequired
239
	 */
240
	public function fileInfo(int $fileId) {
241
		$userFolder = $this->scanner->resolveUserFolder($this->userId);
242
		$info = $this->scanner->getFileInfo($fileId, $this->userId, $userFolder);
243
		if ($info) {
244
			return new JSONResponse($info);
245
		} else {
246
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
247
		}
248
	}
249
250
	/**
251
	 * @NoAdminRequired
252
	 * @NoCSRFRequired
253
	 */
254
	public function fileDetails(int $fileId) {
255
		$userFolder = $this->scanner->resolveUserFolder($this->userId);
256
		$details = $this->detailsHelper->getDetails($fileId, $userFolder);
257
		if ($details) {
258
			// metadata extracted, attempt to include also the data from Last.fm
259
			$track = $this->trackBusinessLayer->findByFileId($fileId, $this->userId);
260
			if ($track) {
261
				$details['lastfm'] = $this->lastfmService->getTrackInfo($track->getId(), $this->userId);
262
			} else {
263
				$this->logger->log("Track with file ID $fileId was not found => can't fetch info from Last.fm", 'warn');
264
			}
265
266
			return new JSONResponse($details);
267
		} else {
268
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
269
		}
270
	}
271
272
	/**
273
	 * @NoAdminRequired
274
	 * @NoCSRFRequired
275
	 */
276
	public function scrobble(int $trackId) {
277
		try {
278
			$this->trackBusinessLayer->recordTrackPlayed($trackId, $this->userId);
279
			return new JSONResponse(['success' => true]);
280
		} catch (BusinessLayerException $e) {
281
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
282
		}
283
	}
284
285
	/**
286
	 * @NoAdminRequired
287
	 * @NoCSRFRequired
288
	 */
289
	public function albumDetails(int $albumId) {
290
		try {
291
			$info = $this->lastfmService->getAlbumInfo($albumId, $this->userId);
292
			return new JSONResponse($info);
293
		} catch (BusinessLayerException $e) {
294
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
295
		}
296
	}
297
298
	/**
299
	 * @NoAdminRequired
300
	 * @NoCSRFRequired
301
	 */
302
	public function artistDetails(int $artistId) {
303
		try {
304
			$info = $this->lastfmService->getArtistInfo($artistId, $this->userId);
305
			return new JSONResponse($info);
306
		} catch (BusinessLayerException $e) {
307
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
308
		}
309
	}
310
311
	/**
312
	 * @NoAdminRequired
313
	 * @NoCSRFRequired
314
	 */
315
	public function similarArtists(int $artistId) {
316
		try {
317
			$similar = $this->lastfmService->getSimilarArtists($artistId, $this->userId, /*includeNotPresent=*/true);
318
			return new JSONResponse(\array_map(fn($artist) => [
319
				'id' => $artist->getId(),
320
				'name' => $artist->getName(),
321
				'url' => $artist->getLastfmUrl()
322
			], $similar));
323
		} catch (BusinessLayerException $e) {
324
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
325
		}
326
	}
327
328
	private static function setClientCaching(Response &$httpResponse, int $days=365) : void {
329
		$httpResponse->cacheFor($days * 24 * 60 * 60);
330
		$httpResponse->addHeader('Pragma', 'cache');
331
	}
332
}
333