Passed
Push — master ( a4260e...ab8f86 )
by Pauli
13:01
created

PlaylistApiController::removeTracks()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 2
dl 0
loc 2
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\JSONResponse;
20
21
use OCP\Files\Folder;
22
use OCP\IConfig;
23
use OCP\IRequest;
24
use OCP\IURLGenerator;
25
26
use OCA\Music\AppFramework\BusinessLayer\BusinessLayerException;
27
use OCA\Music\AppFramework\Core\Logger;
28
use OCA\Music\BusinessLayer\AlbumBusinessLayer;
29
use OCA\Music\BusinessLayer\ArtistBusinessLayer;
30
use OCA\Music\BusinessLayer\GenreBusinessLayer;
31
use OCA\Music\BusinessLayer\PlaylistBusinessLayer;
32
use OCA\Music\BusinessLayer\TrackBusinessLayer;
33
use OCA\Music\Http\ErrorResponse;
34
use OCA\Music\Http\FileResponse;
35
use OCA\Music\Utility\CoverHelper;
36
use OCA\Music\Utility\PlaylistFileService;
37
use OCA\Music\Utility\Util;
38
39
class PlaylistApiController extends Controller {
40
	private $urlGenerator;
41
	private $playlistBusinessLayer;
42
	private $artistBusinessLayer;
43
	private $albumBusinessLayer;
44
	private $trackBusinessLayer;
45
	private $genreBusinessLayer;
46
	private $coverHelper;
47
	private $playlistFileService;
48
	private $userId;
49
	private $userFolder;
50
	private $configManager;
51
	private $logger;
52
53
	public function __construct(string $appname,
54
								IRequest $request,
55
								IURLGenerator $urlGenerator,
56
								PlaylistBusinessLayer $playlistBusinessLayer,
57
								ArtistBusinessLayer $artistBusinessLayer,
58
								AlbumBusinessLayer $albumBusinessLayer,
59
								TrackBusinessLayer $trackBusinessLayer,
60
								GenreBusinessLayer $genreBusinessLayer,
61
								CoverHelper $coverHelper,
62
								PlaylistFileService $playlistFileService,
63
								string $userId,
64
								Folder $userFolder,
65
								IConfig $configManager,
66
								Logger $logger) {
67
		parent::__construct($appname, $request);
68
		$this->urlGenerator = $urlGenerator;
69
		$this->playlistBusinessLayer = $playlistBusinessLayer;
70
		$this->artistBusinessLayer = $artistBusinessLayer;
71
		$this->albumBusinessLayer = $albumBusinessLayer;
72
		$this->trackBusinessLayer = $trackBusinessLayer;
73
		$this->genreBusinessLayer = $genreBusinessLayer;
74
		$this->coverHelper = $coverHelper;
75
		$this->playlistFileService = $playlistFileService;
76
		$this->userId = $userId;
77
		$this->userFolder = $userFolder;
78
		$this->configManager = $configManager;
79
		$this->logger = $logger;
80
	}
81
82
	/**
83
	 * lists all playlists
84
	 *
85
	 * @NoAdminRequired
86
	 * @NoCSRFRequired
87
	 */
88
	public function getAll() {
89
		$playlists = $this->playlistBusinessLayer->findAll($this->userId);
90
		return Util::arrayMapMethod($playlists, 'toAPI', [$this->urlGenerator]);
91
	}
92
93
	/**
94
	 * creates a playlist
95
	 *
96
	 * @NoAdminRequired
97
	 * @NoCSRFRequired
98
	 */
99
	public function create(string $name, /*mixed*/ $trackIds, ?string $comment=null) {
100
		$playlist = $this->playlistBusinessLayer->create($name, $this->userId);
101
102
		// add trackIds and comment to the newly created playlist if provided
103
		if (!empty($trackIds)) {
104
			$playlist = $this->playlistBusinessLayer->addTracks(
105
					self::toIntArray($trackIds), $playlist->getId(), $this->userId);
106
		}
107
		if ($comment !== null) {
108
			$playlist = $this->playlistBusinessLayer->setComment($comment, $playlist->getId(), $this->userId);
109
		}
110
111
		return $playlist->toAPI($this->urlGenerator);
112
	}
113
114
	/**
115
	 * deletes a playlist
116
	 * @param  int $id playlist ID
117
	 *
118
	 * @NoAdminRequired
119
	 * @NoCSRFRequired
120
	 */
121
	public function delete(int $id) {
122
		$this->playlistBusinessLayer->delete($id, $this->userId);
123
		return [];
124
	}
125
126
	/**
127
	 * lists a single playlist
128
	 * @param  int $id playlist ID
129
	 *
130
	 * @NoAdminRequired
131
	 * @NoCSRFRequired
132
	 */
133
	public function get(int $id, $fulltree) {
134
		try {
135
			$playlist = $this->playlistBusinessLayer->find($id, $this->userId);
136
137
			$fulltree = \filter_var($fulltree, FILTER_VALIDATE_BOOLEAN);
138
			if ($fulltree) {
139
				return $this->toFullTree($playlist);
140
			} else {
141
				return $playlist->toAPI($this->urlGenerator);
142
			}
143
		} catch (BusinessLayerException $ex) {
144
			return new ErrorResponse(Http::STATUS_NOT_FOUND, $ex->getMessage());
145
		}
146
	}
147
148
	private function toFullTree($playlist) {
149
		$trackIds = $playlist->getTrackIdsAsArray();
150
		$tracks = $this->trackBusinessLayer->findById($trackIds, $this->userId);
151
		$this->albumBusinessLayer->injectAlbumsToTracks($tracks, $this->userId);
152
153
		$result = $playlist->toAPI($this->urlGenerator);
154
		unset($result['trackIds']);
155
		$result['tracks'] = Util::arrayMapMethod($tracks, 'toAPI', [$this->urlGenerator]);
156
157
		return $result;
158
	}
159
160
	/**
161
	 * generate a smart playlist according to the given rules
162
	 *
163
	 * @NoAdminRequired
164
	 * @NoCSRFRequired
165
	 */
166
	public function generate(
167
			?bool $useLatestParams, ?string $history, ?string $genres, ?string $artists,
168
			?int $fromYear, ?int $toYear, ?string $favorite=null, int $size=100, string $historyStrict='false') {
169
170
		if ($useLatestParams) {
171
			$history = $this->configManager->getUserValue($this->userId, $this->appName, 'smartlist_history') ?: null;
172
			$genres = $this->configManager->getUserValue($this->userId, $this->appName, 'smartlist_genres') ?: null;
173
			$artists = $this->configManager->getUserValue($this->userId, $this->appName, 'smartlist_artists') ?: null;
174
			$fromYear = (int)$this->configManager->getUserValue($this->userId, $this->appName, 'smartlist_from_year') ?: null;
175
			$toYear = (int)$this->configManager->getUserValue($this->userId, $this->appName, 'smartlist_to_year') ?: null;
176
			$favorite = $this->configManager->getUserValue($this->userId, $this->appName, 'smartlist_favorite') ?: null;
177
			$size = (int)$this->configManager->getUserValue($this->userId, $this->appName, 'smartlist_size', 100);
178
			$historyStrict = $this->configManager->getUserValue($this->userId, $this->appName, 'smartlist_history_strict', 'false');
179
		} else {
180
			$this->configManager->setUserValue($this->userId, $this->appName, 'smartlist_history', $history ?? '');
181
			$this->configManager->setUserValue($this->userId, $this->appName, 'smartlist_genres', $genres ?? '');
182
			$this->configManager->setUserValue($this->userId, $this->appName, 'smartlist_artists', $artists ?? '');
183
			$this->configManager->setUserValue($this->userId, $this->appName, 'smartlist_from_year', (string)$fromYear);
184
			$this->configManager->setUserValue($this->userId, $this->appName, 'smartlist_to_year', (string)$toYear);
185
			$this->configManager->setUserValue($this->userId, $this->appName, 'smartlist_favorite', $favorite ?? '');
186
			$this->configManager->setUserValue($this->userId, $this->appName, 'smartlist_size', (string)$size);
187
			$this->configManager->setUserValue($this->userId, $this->appName, 'smartlist_history_strict', $historyStrict);
188
		}
189
		$historyStrict = \filter_var($historyStrict, FILTER_VALIDATE_BOOLEAN);
190
191
		// ensure the artists and genres contain only valid IDs
192
		$genres = $this->genreBusinessLayer->findAllIds($this->userId, self::toIntArray($genres));
193
		$artists = $this->artistBusinessLayer->findAllIds($this->userId, self::toIntArray($artists));
194
195
		$playlist = $this->playlistBusinessLayer->generate(
196
				$history, $historyStrict, $genres, $artists, $fromYear, $toYear, $favorite, $size, $this->userId);
197
		$result = $playlist->toAPI($this->urlGenerator);
198
199
		$result['params'] = [
200
			'history' => $history ?: null,
201
			'historyStrict' => $historyStrict,
202
			'genres' => \implode(',', $genres) ?: null,
203
			'artists' => \implode(',', $artists) ?: null,
204
			'fromYear' => $fromYear ?: null,
205
			'toYear' => $toYear ?: null,
206
			'favorite' => $favorite ?: null,
207
			'size' => $size
208
		];
209
210
		return $result;
211
	}
212
213
	/**
214
	 * get cover image for a playlist
215
	 * @param int $id playlist ID
216
	 *
217
	 * @NoAdminRequired
218
	 * @NoCSRFRequired
219
	 */
220
	public function getCover(int $id) {
221
		try {
222
			$playlist = $this->playlistBusinessLayer->find($id, $this->userId);
223
			$cover = $this->coverHelper->getCover($playlist, $this->userId, $this->userFolder);
224
225
			if ($cover !== null) {
226
				return new FileResponse($cover);
227
			} else {
228
				return new ErrorResponse(Http::STATUS_NOT_FOUND, 'The playlist has no cover art');
229
			}
230
		} catch (BusinessLayerException $ex) {
231
			return new ErrorResponse(Http::STATUS_NOT_FOUND, $ex->getMessage());
232
		}
233
	}
234
235
	/**
236
	 * update a playlist
237
	 * @param int $id playlist ID
238
	 *
239
	 * @NoAdminRequired
240
	 * @NoCSRFRequired
241
	 */
242
	public function update(int $id, string $name = null, string $comment = null, string $trackIds = null) {
243
		$result = null;
244
		if ($name !== null) {
245
			$result = $this->modifyPlaylist('rename', [$name, $id, $this->userId]);
246
		}
247
		if ($comment !== null) {
248
			$result = $this->modifyPlaylist('setComment', [$comment, $id, $this->userId]);
249
		}
250
		if ($trackIds!== null) {
251
			$result = $this->modifyPlaylist('setTracks', [self::toIntArray($trackIds), $id, $this->userId]);
252
		}
253
		if ($result === null) {
254
			$result = new ErrorResponse(Http::STATUS_BAD_REQUEST, "at least one of the args ['name', 'comment', 'trackIds'] must be given");
255
		}
256
		return $result;
257
	}
258
259
	/**
260
	 * add tracks to a playlist
261
	 * @param  int $id playlist ID
262
	 *
263
	 * @NoAdminRequired
264
	 * @NoCSRFRequired
265
	 */
266
	public function addTracks(int $id, $trackIds) {
267
		return $this->modifyPlaylist('addTracks', [self::toIntArray($trackIds), $id, $this->userId]);
268
	}
269
270
	/**
271
	 * removes tracks from a playlist
272
	 * @param  int $id playlist ID
273
	 *
274
	 * @NoAdminRequired
275
	 * @NoCSRFRequired
276
	 */
277
	public function removeTracks(int $id, $indices) {
278
		return $this->modifyPlaylist('removeTracks', [self::toIntArray($indices), $id, $this->userId]);
279
	}
280
281
	/**
282
	 * moves single track on playlist to a new position
283
	 * @param  int $id playlist ID
284
	 *
285
	 * @NoAdminRequired
286
	 * @NoCSRFRequired
287
	 */
288
	public function reorder(int $id, $fromIndex, $toIndex) {
289
		return $this->modifyPlaylist('moveTrack',
290
				[$fromIndex, $toIndex, $id, $this->userId]);
291
	}
292
293
	/**
294
	 * export the playlist to a file
295
	 * @param int $id playlist ID
296
	 * @param string $path parent folder path
297
	 * @param string $oncollision action to take on file name collision,
298
	 *								supported values:
299
	 *								- 'overwrite' The existing file will be overwritten
300
	 *								- 'keepboth' The new file is named with a suffix to make it unique
301
	 *								- 'abort' (default) The operation will fail
302
	 *
303
	 * @NoAdminRequired
304
	 * @NoCSRFRequired
305
	 */
306
	public function exportToFile(int $id, string $path, string $oncollision) {
307
		try {
308
			$exportedFilePath = $this->playlistFileService->exportToFile(
309
					$id, $this->userId, $this->userFolder, $path, $oncollision);
310
			return new JSONResponse(['wrote_to_file' => $exportedFilePath]);
311
		} catch (BusinessLayerException $ex) {
312
			return new ErrorResponse(Http::STATUS_NOT_FOUND, 'playlist not found');
313
		} catch (\OCP\Files\NotFoundException $ex) {
314
			return new ErrorResponse(Http::STATUS_NOT_FOUND, 'folder not found');
315
		} catch (\RuntimeException $ex) {
316
			return new ErrorResponse(Http::STATUS_CONFLICT, $ex->getMessage());
317
		} catch (\OCP\Files\NotPermittedException $ex) {
318
			return new ErrorResponse(Http::STATUS_FORBIDDEN, 'user is not allowed to write to the target file');
319
		}
320
	}
321
322
	/**
323
	 * import playlist contents from a file
324
	 * @param int $id playlist ID
325
	 * @param string $filePath path of the file to import
326
	 *
327
	 * @NoAdminRequired
328
	 * @NoCSRFRequired
329
	 */
330
	public function importFromFile(int $id, string $filePath) {
331
		try {
332
			$result = $this->playlistFileService->importFromFile($id, $this->userId, $this->userFolder, $filePath);
333
			$result['playlist'] = $result['playlist']->toAPI($this->urlGenerator);
334
			return $result;
335
		} catch (BusinessLayerException $ex) {
336
			return new ErrorResponse(Http::STATUS_NOT_FOUND, 'playlist not found');
337
		} catch (\OCP\Files\NotFoundException $ex) {
338
			return new ErrorResponse(Http::STATUS_NOT_FOUND, 'playlist file not found');
339
		} catch (\UnexpectedValueException $ex) {
340
			return new ErrorResponse(Http::STATUS_UNSUPPORTED_MEDIA_TYPE, $ex->getMessage());
341
		}
342
	}
343
344
	/**
345
	 * read and parse a playlist file
346
	 * @param int $fileId ID of the file to parse
347
	 *
348
	 * @NoAdminRequired
349
	 * @NoCSRFRequired
350
	 */
351
	public function parseFile(int $fileId) {
352
		try {
353
			$result = $this->playlistFileService->parseFile($fileId, $this->userFolder);
354
355
			// Make a lookup table of all the file IDs in the user library to avoid having to run
356
			// a DB query for each track in the playlist to check if it is in the library. This
357
			// could make a difference in case of a huge playlist.
358
			$libFileIds = $this->trackBusinessLayer->findAllFileIds($this->userId);
359
			$libFileIds = \array_flip($libFileIds);
360
361
			$bogusUrlId = -1;
362
363
			// compose the final result
364
			$result['files'] = \array_map(function ($fileInfo) use ($libFileIds, &$bogusUrlId) {
365
				if (isset($fileInfo['url'])) {
366
					$fileInfo['id'] = $bogusUrlId--;
367
					$fileInfo['mimetype'] = null;
368
					$fileInfo['external'] = true;
369
					return $fileInfo;
370
				} else {
371
					$file = $fileInfo['file'];
372
					return [
373
						'id' => $file->getId(),
374
						'name' => $file->getName(),
375
						'path' => $this->userFolder->getRelativePath($file->getParent()->getPath()),
376
						'mimetype' => $file->getMimeType(),
377
						'caption' => $fileInfo['caption'],
378
						'in_library' => isset($libFileIds[$file->getId()]),
379
						'external' => false
380
					];
381
				}
382
			}, $result['files']);
383
			return new JSONResponse($result);
384
		} catch (\OCP\Files\NotFoundException $ex) {
385
			return new ErrorResponse(Http::STATUS_NOT_FOUND, 'playlist file not found');
386
		} catch (\UnexpectedValueException $ex) {
387
			return new ErrorResponse(Http::STATUS_UNSUPPORTED_MEDIA_TYPE, $ex->getMessage());
388
		}
389
	}
390
391
	/**
392
	 * Modify playlist by calling a supplied method from PlaylistBusinessLayer
393
	 * @param string $funcName  Name of a function to call from PlaylistBusinessLayer
394
	 * @param array $funcParams Parameters to pass to the function 'funcName'
395
	 * @return JSONResponse JSON representation of the modified playlist
396
	 */
397
	private function modifyPlaylist(string $funcName, array $funcParams) : JSONResponse {
398
		try {
399
			$playlist = \call_user_func_array([$this->playlistBusinessLayer, $funcName], $funcParams);
400
			return new JSONResponse($playlist->toAPI($this->urlGenerator));
401
		} catch (BusinessLayerException $ex) {
402
			return new ErrorResponse(Http::STATUS_NOT_FOUND, $ex->getMessage());
403
		}
404
	}
405
406
	/**
407
	 * Get integer array passed as parameter to the Playlist API
408
	 * @param string|int|null $listAsString Comma-separated integer values in string, or a single integer
409
	 * @return int[]
410
	 */
411
	private static function toIntArray(/*mixed*/ $listAsString) : array {
412
		if ($listAsString === null || $listAsString === '') {
413
			return [];
414
		} else {
415
			return \array_map('intval', \explode(',', (string)$listAsString));
416
		}
417
	}
418
}
419