Passed
Push — master ( b806b4...5b46ab )
by Pauli
02:42
created

PlaylistApiController::toFullTree()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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