Passed
Push — feature/1061-random-view ( afd67b...1c2ae7 )
by Pauli
03:33
created

PlaylistApiController   A

Complexity

Total Complexity 39

Size/Duplication

Total Lines 323
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 127
c 1
b 0
f 0
dl 0
loc 323
rs 9.28
wmc 39

17 Methods

Rating   Name   Duplication   Size   Complexity  
A getAll() 0 5 1
A __construct() 0 21 1
A delete() 0 3 1
A toFullTree() 0 10 1
A create() 0 10 2
A get() 0 12 3
A modifyPlaylist() 0 6 2
A getCover() 0 12 3
A removeTracks() 0 2 1
A update() 0 15 5
A importFromFile() 0 11 4
A reorder() 0 3 1
A addTracks() 0 2 1
A exportToFile() 0 13 5
A toIntArray() 0 5 3
A generate() 0 3 1
A parseFile() 0 35 4
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, ?int $fromYear, ?int $toYear, int $size=300) {
155
		$playlist = $this->playlistBusinessLayer->generate($playRate, self::toIntArray($genres), $fromYear, $toYear, $size, $this->userId);
156
		return $playlist->toAPI();
157
	}
158
159
	/**
160
	 * get cover image for a playlist
161
	 * @param int $id playlist ID
162
	 *
163
	 * @NoAdminRequired
164
	 * @NoCSRFRequired
165
	 */
166
	public function getCover(int $id) {
167
		try {
168
			$playlist = $this->playlistBusinessLayer->find($id, $this->userId);
169
			$cover = $this->coverHelper->getCover($playlist, $this->userId, $this->userFolder);
170
171
			if ($cover !== null) {
172
				return new FileResponse($cover);
173
			} else {
174
				return new ErrorResponse(Http::STATUS_NOT_FOUND, 'The playlist has no cover art');
175
			}
176
		} catch (BusinessLayerException $ex) {
177
			return new ErrorResponse(Http::STATUS_NOT_FOUND, $ex->getMessage());
178
		}
179
	}
180
181
	/**
182
	 * update a playlist
183
	 * @param int $id playlist ID
184
	 *
185
	 * @NoAdminRequired
186
	 * @NoCSRFRequired
187
	 */
188
	public function update(int $id, string $name = null, string $comment = null, string $trackIds = null) {
189
		$result = null;
190
		if ($name !== null) {
191
			$result = $this->modifyPlaylist('rename', [$name, $id, $this->userId]);
192
		}
193
		if ($comment !== null) {
194
			$result = $this->modifyPlaylist('setComment', [$comment, $id, $this->userId]);
195
		}
196
		if ($trackIds!== null) {
197
			$result = $this->modifyPlaylist('setTracks', [self::toIntArray($trackIds), $id, $this->userId]);
198
		}
199
		if ($result === null) {
200
			$result = new ErrorResponse(Http::STATUS_BAD_REQUEST, "at least one of the args ['name', 'comment', 'trackIds'] must be given");
201
		}
202
		return $result;
203
	}
204
205
	/**
206
	 * add tracks to a playlist
207
	 * @param  int $id playlist ID
208
	 *
209
	 * @NoAdminRequired
210
	 * @NoCSRFRequired
211
	 */
212
	public function addTracks(int $id, $trackIds) {
213
		return $this->modifyPlaylist('addTracks', [self::toIntArray($trackIds), $id, $this->userId]);
214
	}
215
216
	/**
217
	 * removes tracks from a playlist
218
	 * @param  int $id playlist ID
219
	 *
220
	 * @NoAdminRequired
221
	 * @NoCSRFRequired
222
	 */
223
	public function removeTracks(int $id, $indices) {
224
		return $this->modifyPlaylist('removeTracks', [self::toIntArray($indices), $id, $this->userId]);
225
	}
226
227
	/**
228
	 * moves single track on playlist to a new position
229
	 * @param  int $id playlist ID
230
	 *
231
	 * @NoAdminRequired
232
	 * @NoCSRFRequired
233
	 */
234
	public function reorder(int $id, $fromIndex, $toIndex) {
235
		return $this->modifyPlaylist('moveTrack',
236
				[$fromIndex, $toIndex, $id, $this->userId]);
237
	}
238
239
	/**
240
	 * export the playlist to a file
241
	 * @param int $id playlist ID
242
	 * @param string $path parent folder path
243
	 * @param string $oncollision action to take on file name collision,
244
	 *								supported values:
245
	 *								- 'overwrite' The existing file will be overwritten
246
	 *								- 'keepboth' The new file is named with a suffix to make it unique
247
	 *								- 'abort' (default) The operation will fail
248
	 *
249
	 * @NoAdminRequired
250
	 * @NoCSRFRequired
251
	 */
252
	public function exportToFile(int $id, string $path, string $oncollision) {
253
		try {
254
			$exportedFilePath = $this->playlistFileService->exportToFile(
255
					$id, $this->userId, $this->userFolder, $path, $oncollision);
256
			return new JSONResponse(['wrote_to_file' => $exportedFilePath]);
257
		} catch (BusinessLayerException $ex) {
258
			return new ErrorResponse(Http::STATUS_NOT_FOUND, 'playlist not found');
259
		} catch (\OCP\Files\NotFoundException $ex) {
260
			return new ErrorResponse(Http::STATUS_NOT_FOUND, 'folder not found');
261
		} catch (\RuntimeException $ex) {
262
			return new ErrorResponse(Http::STATUS_CONFLICT, $ex->getMessage());
263
		} catch (\OCP\Files\NotPermittedException $ex) {
264
			return new ErrorResponse(Http::STATUS_FORBIDDEN, 'user is not allowed to write to the target file');
265
		}
266
	}
267
268
	/**
269
	 * import playlist contents from a file
270
	 * @param int $id playlist ID
271
	 * @param string $filePath path of the file to import
272
	 *
273
	 * @NoAdminRequired
274
	 * @NoCSRFRequired
275
	 */
276
	public function importFromFile(int $id, string $filePath) {
277
		try {
278
			$result = $this->playlistFileService->importFromFile($id, $this->userId, $this->userFolder, $filePath);
279
			$result['playlist'] = $result['playlist']->toAPI();
280
			return $result;
281
		} catch (BusinessLayerException $ex) {
282
			return new ErrorResponse(Http::STATUS_NOT_FOUND, 'playlist not found');
283
		} catch (\OCP\Files\NotFoundException $ex) {
284
			return new ErrorResponse(Http::STATUS_NOT_FOUND, 'playlist file not found');
285
		} catch (\UnexpectedValueException $ex) {
286
			return new ErrorResponse(Http::STATUS_UNSUPPORTED_MEDIA_TYPE, $ex->getMessage());
287
		}
288
	}
289
290
	/**
291
	 * read and parse a playlist file
292
	 * @param int $fileId ID of the file to parse
293
	 *
294
	 * @NoAdminRequired
295
	 * @NoCSRFRequired
296
	 */
297
	public function parseFile(int $fileId) {
298
		try {
299
			$result = $this->playlistFileService->parseFile($fileId, $this->userFolder);
300
301
			// Make a lookup table of all the file IDs in the user library to avoid having to run
302
			// a DB query for each track in the playlist to check if it is in the library. This
303
			// could make a difference in case of a huge playlist.
304
			$libFileIds = $this->trackBusinessLayer->findAllFileIds($this->userId);
305
			$libFileIds = \array_flip($libFileIds);
306
307
			$bogusUrlId = -1;
308
309
			// compose the final result
310
			$result['files'] = \array_map(function ($fileInfo) use ($libFileIds, &$bogusUrlId) {
311
				if (isset($fileInfo['url'])) {
312
					$fileInfo['id'] = $bogusUrlId--;
313
					$fileInfo['mimetype'] = null;
314
					return $fileInfo;
315
				} else {
316
					$file = $fileInfo['file'];
317
					return [
318
						'id' => $file->getId(),
319
						'name' => $file->getName(),
320
						'path' => $this->userFolder->getRelativePath($file->getParent()->getPath()),
321
						'mimetype' => $file->getMimeType(),
322
						'caption' => $fileInfo['caption'],
323
						'in_library' => isset($libFileIds[$file->getId()])
324
					];
325
				}
326
			}, $result['files']);
327
			return new JSONResponse($result);
328
		} catch (\OCP\Files\NotFoundException $ex) {
329
			return new ErrorResponse(Http::STATUS_NOT_FOUND, 'playlist file not found');
330
		} catch (\UnexpectedValueException $ex) {
331
			return new ErrorResponse(Http::STATUS_UNSUPPORTED_MEDIA_TYPE, $ex->getMessage());
332
		}
333
	}
334
335
	/**
336
	 * Modify playlist by calling a supplied method from PlaylistBusinessLayer
337
	 * @param string $funcName  Name of a function to call from PlaylistBusinessLayer
338
	 * @param array $funcParams Parameters to pass to the function 'funcName'
339
	 * @return JSONResponse JSON representation of the modified playlist
340
	 */
341
	private function modifyPlaylist(string $funcName, array $funcParams) : JSONResponse {
342
		try {
343
			$playlist = \call_user_func_array([$this->playlistBusinessLayer, $funcName], $funcParams);
344
			return new JSONResponse($playlist->toAPI());
345
		} catch (BusinessLayerException $ex) {
346
			return new ErrorResponse(Http::STATUS_NOT_FOUND, $ex->getMessage());
347
		}
348
	}
349
350
	/**
351
	 * Get integer array passed as parameter to the Playlist API
352
	 * @param string|int|null $listAsString Comma-separated integer values in string, or a single integer
353
	 * @return int[]
354
	 */
355
	private static function toIntArray(/*mixed*/ $listAsString) : array {
356
		if ($listAsString === null || $listAsString === '') {
357
			return [];
358
		} else {
359
			return \array_map('intval', \explode(',', (string)$listAsString));
360
		}
361
	}
362
}
363