Passed
Push — master ( ed5322...856ddd )
by Pauli
04:54 queued 14s
created

PlaylistApiController::generate()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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