Total Complexity | 248 |
Total Lines | 1269 |
Duplicated Lines | 0 % |
Changes | 2 | ||
Bugs | 0 | Features | 0 |
Complex classes like AmpacheController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use AmpacheController, and based on these observations, apply Extract Interface, too.
1 | <?php declare(strict_types=1); |
||
56 | class AmpacheController extends Controller { |
||
57 | private $ampacheUserMapper; |
||
58 | private $ampacheSessionMapper; |
||
59 | private $albumBusinessLayer; |
||
60 | private $artistBusinessLayer; |
||
61 | private $genreBusinessLayer; |
||
62 | private $playlistBusinessLayer; |
||
63 | private $podcastChannelBusinessLayer; |
||
64 | private $podcastEpisodeBusinessLayer; |
||
65 | private $trackBusinessLayer; |
||
66 | private $library; |
||
67 | private $podcastService; |
||
68 | private $ampacheUser; |
||
69 | private $urlGenerator; |
||
70 | private $rootFolder; |
||
71 | private $l10n; |
||
72 | private $coverHelper; |
||
73 | private $random; |
||
74 | private $logger; |
||
75 | private $jsonMode; |
||
76 | |||
77 | const SESSION_EXPIRY_TIME = 6000; |
||
78 | const ALL_TRACKS_PLAYLIST_ID = 10000000; |
||
79 | const API_VERSION = 440000; |
||
80 | const API_MIN_COMPATIBLE_VERSION = 350001; |
||
81 | |||
82 | public function __construct(string $appname, |
||
83 | IRequest $request, |
||
84 | $l10n, |
||
85 | IURLGenerator $urlGenerator, |
||
86 | AmpacheUserMapper $ampacheUserMapper, |
||
87 | AmpacheSessionMapper $ampacheSessionMapper, |
||
88 | AlbumBusinessLayer $albumBusinessLayer, |
||
89 | ArtistBusinessLayer $artistBusinessLayer, |
||
90 | GenreBusinessLayer $genreBusinessLayer, |
||
91 | PlaylistBusinessLayer $playlistBusinessLayer, |
||
92 | PodcastChannelBusinessLayer $podcastChannelBusinessLayer, |
||
93 | PodcastEpisodeBusinessLayer $podcastEpisodeBusinessLayer, |
||
94 | TrackBusinessLayer $trackBusinessLayer, |
||
95 | Library $library, |
||
96 | PodcastService $podcastService, |
||
97 | AmpacheUser $ampacheUser, |
||
98 | $rootFolder, |
||
99 | CoverHelper $coverHelper, |
||
100 | Random $random, |
||
101 | Logger $logger) { |
||
102 | parent::__construct($appname, $request); |
||
103 | |||
104 | $this->ampacheUserMapper = $ampacheUserMapper; |
||
105 | $this->ampacheSessionMapper = $ampacheSessionMapper; |
||
106 | $this->albumBusinessLayer = $albumBusinessLayer; |
||
107 | $this->artistBusinessLayer = $artistBusinessLayer; |
||
108 | $this->genreBusinessLayer = $genreBusinessLayer; |
||
109 | $this->playlistBusinessLayer = $playlistBusinessLayer; |
||
110 | $this->podcastChannelBusinessLayer = $podcastChannelBusinessLayer; |
||
111 | $this->podcastEpisodeBusinessLayer = $podcastEpisodeBusinessLayer; |
||
112 | $this->trackBusinessLayer = $trackBusinessLayer; |
||
113 | $this->library = $library; |
||
114 | $this->podcastService = $podcastService; |
||
115 | $this->urlGenerator = $urlGenerator; |
||
116 | $this->l10n = $l10n; |
||
117 | |||
118 | // used to share user info with middleware |
||
119 | $this->ampacheUser = $ampacheUser; |
||
120 | |||
121 | // used to deliver actual media file |
||
122 | $this->rootFolder = $rootFolder; |
||
123 | |||
124 | $this->coverHelper = $coverHelper; |
||
125 | $this->random = $random; |
||
126 | $this->logger = $logger; |
||
127 | } |
||
128 | |||
129 | public function setJsonMode($useJsonMode) { |
||
130 | $this->jsonMode = $useJsonMode; |
||
131 | } |
||
132 | |||
133 | public function ampacheResponse($content) { |
||
134 | if ($this->jsonMode) { |
||
135 | return new JSONResponse(self::prepareResultForJsonApi($content)); |
||
136 | } else { |
||
137 | return new XmlResponse(self::prepareResultForXmlApi($content), ['id', 'index', 'count', 'code']); |
||
138 | } |
||
139 | } |
||
140 | |||
141 | /** |
||
142 | * @NoAdminRequired |
||
143 | * @PublicPage |
||
144 | * @NoCSRFRequired |
||
145 | */ |
||
146 | public function xmlApi($action, $user, $timestamp, $auth, $filter, $exact, $limit, $offset, $id, $add, $update) { |
||
147 | // differentation between xmlApi and jsonApi is made already by the middleware |
||
148 | return $this->dispatch($action, $user, $timestamp, $auth, $filter, $exact, $limit, $offset, $id, $add, $update); |
||
149 | } |
||
150 | |||
151 | /** |
||
152 | * @NoAdminRequired |
||
153 | * @PublicPage |
||
154 | * @NoCSRFRequired |
||
155 | */ |
||
156 | public function jsonApi($action, $user, $timestamp, $auth, $filter, $exact, $limit, $offset, $id, $add, $update) { |
||
157 | // differentation between xmlApi and jsonApi is made already by the middleware |
||
158 | return $this->dispatch($action, $user, $timestamp, $auth, $filter, $exact, $limit, $offset, $id, $add, $update); |
||
159 | } |
||
160 | |||
161 | protected function dispatch($action, $user, $timestamp, $auth, $filter, $exact, $limit, $offset, $id, $add, $update) { |
||
162 | $this->logger->log("Ampache action '$action' requested", 'debug'); |
||
163 | |||
164 | $limit = self::validateLimitOrOffset($limit); |
||
165 | $offset = self::validateLimitOrOffset($offset); |
||
166 | |||
167 | switch ($action) { |
||
168 | case 'handshake': |
||
169 | return $this->handshake($user, $timestamp, $auth); |
||
170 | case 'goodbye': |
||
171 | return $this->goodbye($auth); |
||
172 | case 'ping': |
||
173 | return $this->ping($auth); |
||
174 | case 'get_indexes': |
||
175 | return $this->get_indexes($filter, $limit, $offset, $add, $update); |
||
176 | case 'stats': |
||
177 | return $this->stats($filter, $limit, $offset, $auth); |
||
178 | case 'artists': |
||
179 | return $this->artists($filter, $exact, $limit, $offset, $add, $update, $auth); |
||
180 | case 'artist': |
||
181 | return $this->artist((int)$filter, $auth); |
||
182 | case 'artist_albums': |
||
183 | return $this->artist_albums((int)$filter, $auth); |
||
184 | case 'album_songs': |
||
185 | return $this->album_songs((int)$filter, $auth); |
||
186 | case 'albums': |
||
187 | return $this->albums($filter, $exact, $limit, $offset, $add, $update, $auth); |
||
188 | case 'album': |
||
189 | return $this->album((int)$filter, $auth); |
||
190 | case 'artist_songs': |
||
191 | return $this->artist_songs((int)$filter, $auth); |
||
192 | case 'songs': |
||
193 | return $this->songs($filter, $exact, $limit, $offset, $add, $update, $auth); |
||
194 | case 'song': |
||
195 | return $this->song((int)$filter, $auth); |
||
196 | case 'search_songs': |
||
197 | return $this->search_songs($filter, $auth); |
||
198 | case 'playlists': |
||
199 | return $this->playlists($filter, $exact, $limit, $offset, $add, $update); |
||
200 | case 'playlist': |
||
201 | return $this->playlist((int)$filter); |
||
202 | case 'playlist_songs': |
||
203 | return $this->playlist_songs((int)$filter, $limit, $offset, $auth); |
||
204 | case 'playlist_create': |
||
205 | return $this->playlist_create(); |
||
206 | case 'playlist_edit': |
||
207 | return $this->playlist_edit((int)$filter); |
||
208 | case 'playlist_delete': |
||
209 | return $this->playlist_delete((int)$filter); |
||
210 | case 'playlist_add_song': |
||
211 | return $this->playlist_add_song((int)$filter); |
||
212 | case 'playlist_remove_song': |
||
213 | return $this->playlist_remove_song((int)$filter); |
||
214 | case 'playlist_generate': |
||
215 | return $this->playlist_generate($filter, $limit, $offset, $auth); |
||
216 | case 'podcasts': |
||
217 | return $this->podcasts($filter, $exact, $limit, $offset); |
||
218 | case 'podcast': |
||
219 | return $this->podcast((int)$filter); |
||
220 | case 'podcast_create': |
||
221 | return $this->podcast_create(); |
||
222 | case 'podcast_delete': |
||
223 | return $this->podcast_delete((int)$filter); |
||
224 | case 'podcast_episodes': |
||
225 | return $this->podcast_episodes((int)$filter, $limit, $offset); |
||
226 | case 'podcast_episode': |
||
227 | return $this->podcast_episode((int)$filter); |
||
228 | case 'update_podcast': |
||
229 | return $this->update_podcast((int)$id); |
||
230 | case 'tags': |
||
231 | return $this->tags($filter, $exact, $limit, $offset); |
||
232 | case 'tag': |
||
233 | return $this->tag((int)$filter); |
||
234 | case 'tag_artists': |
||
235 | return $this->tag_artists((int)$filter, $limit, $offset, $auth); |
||
236 | case 'tag_albums': |
||
237 | return $this->tag_albums((int)$filter, $limit, $offset, $auth); |
||
238 | case 'tag_songs': |
||
239 | return $this->tag_songs((int)$filter, $limit, $offset, $auth); |
||
240 | case 'flag': |
||
241 | return $this->flag(); |
||
242 | case 'download': |
||
243 | return $this->download((int)$id); // arg 'format' not supported |
||
244 | case 'stream': |
||
245 | return $this->stream((int)$id, $offset); // args 'bitrate', 'format', and 'length' not supported |
||
246 | case 'get_art': |
||
247 | return $this->get_art((int)$id); |
||
248 | } |
||
249 | |||
250 | $this->logger->log("Unsupported Ampache action '$action' requested", 'warn'); |
||
251 | throw new AmpacheException('Action not supported', 405); |
||
252 | } |
||
253 | |||
254 | /*********************** |
||
255 | * Ampahce API methods * |
||
256 | ***********************/ |
||
257 | |||
258 | protected function handshake($user, $timestamp, $auth) { |
||
259 | $currentTime = \time(); |
||
260 | $expiryDate = $currentTime + self::SESSION_EXPIRY_TIME; |
||
261 | |||
262 | $this->checkHandshakeTimestamp($timestamp, $currentTime); |
||
263 | $this->checkHandshakeAuthentication($user, $timestamp, $auth); |
||
264 | $token = $this->startNewSession($user, $expiryDate); |
||
265 | |||
266 | $updateTime = \max($this->library->latestUpdateTime($user), $this->playlistBusinessLayer->latestUpdateTime($user)); |
||
267 | $addTime = \max($this->library->latestInsertTime($user), $this->playlistBusinessLayer->latestInsertTime($user)); |
||
268 | |||
269 | return $this->ampacheResponse([ |
||
270 | 'auth' => $token, |
||
271 | 'api' => self::API_VERSION, |
||
272 | 'update' => $updateTime->format('c'), |
||
273 | 'add' => $addTime->format('c'), |
||
274 | 'clean' => \date('c', $currentTime), // TODO: actual time of the latest item removal |
||
275 | 'songs' => $this->trackBusinessLayer->count($user), |
||
276 | 'artists' => $this->artistBusinessLayer->count($user), |
||
277 | 'albums' => $this->albumBusinessLayer->count($user), |
||
278 | 'playlists' => $this->playlistBusinessLayer->count($user) + 1, // +1 for "All tracks" |
||
279 | 'podcasts' => $this->podcastChannelBusinessLayer->count($user), |
||
280 | 'podcast_episodes' => $this->podcastEpisodeBusinessLayer->count($user), |
||
281 | 'session_expire' => \date('c', $expiryDate), |
||
282 | 'tags' => $this->genreBusinessLayer->count($user), |
||
283 | 'videos' => 0, |
||
284 | 'catalogs' => 0 |
||
285 | ]); |
||
286 | } |
||
287 | |||
288 | protected function goodbye($auth) { |
||
289 | // getting the session should not throw as the middleware has already checked that the token is valid |
||
290 | $session = $this->ampacheSessionMapper->findByToken($auth); |
||
291 | $this->ampacheSessionMapper->delete($session); |
||
292 | |||
293 | return $this->ampacheResponse(['success' => "goodbye: $auth"]); |
||
294 | } |
||
295 | |||
296 | protected function ping($auth) { |
||
297 | $response = [ |
||
298 | 'server' => $this->getAppNameAndVersion(), |
||
299 | 'version' => self::API_VERSION, |
||
300 | 'compatible' => self::API_MIN_COMPATIBLE_VERSION |
||
301 | ]; |
||
302 | |||
303 | if (!empty($auth)) { |
||
304 | // getting the session should not throw as the middleware has already checked that the token is valid |
||
305 | $session = $this->ampacheSessionMapper->findByToken($auth); |
||
306 | $response['session_expire'] = \date('c', $session->getExpiry()); |
||
307 | } |
||
308 | |||
309 | return $this->ampacheResponse($response); |
||
310 | } |
||
311 | |||
312 | protected function get_indexes($filter, $limit, $offset, $add, $update) { |
||
313 | $type = $this->getRequiredParam('type'); |
||
314 | |||
315 | $businessLayer = $this->getBusinessLayer($type); |
||
316 | $entities = $this->findEntities($businessLayer, $filter, false, $limit, $offset, $add, $update); |
||
317 | return $this->renderEntitiesIndex($entities, $type); |
||
318 | } |
||
319 | |||
320 | protected function stats($filter, $limit, $offset, $auth) { |
||
321 | $type = $this->getRequiredParam('type'); |
||
322 | $userId = $this->ampacheUser->getUserId(); |
||
323 | |||
324 | // Support for API v3.x: Originally, there was no 'filter' argument and the 'type' |
||
325 | // argument had that role. The action only supported albums in this old format. |
||
326 | // The 'filter' argument was added and role of 'type' changed in API v4.0. |
||
327 | if (empty($filter)) { |
||
328 | $filter = $type; |
||
329 | $type = 'album'; |
||
330 | } |
||
331 | |||
332 | if (!\in_array($type, ['song', 'album', 'artist'])) { |
||
333 | throw new AmpacheException("Unsupported type $type", 400); |
||
334 | } |
||
335 | $businessLayer = $this->getBusinessLayer($type); |
||
336 | |||
337 | switch ($filter) { |
||
338 | case 'newest': |
||
339 | $entities = $businessLayer->findAll($userId, SortBy::Newest, $limit, $offset); |
||
340 | break; |
||
341 | case 'flagged': |
||
342 | $entities = $businessLayer->findAllStarred($userId, $limit, $offset); |
||
343 | break; |
||
344 | case 'random': |
||
345 | $entities = $businessLayer->findAll($userId, SortBy::None); |
||
346 | $indices = $this->random->getIndices(\count($entities), $offset, $limit, $userId, 'ampache_stats_'.$type); |
||
347 | $entities = Util::arrayMultiGet($entities, $indices); |
||
348 | break; |
||
349 | case 'highest': //TODO |
||
350 | case 'frequent': //TODO |
||
351 | case 'recent': //TODO |
||
352 | case 'forgotten': //TODO |
||
353 | default: |
||
354 | throw new AmpacheException("Unsupported filter $filter", 400); |
||
355 | } |
||
356 | |||
357 | return $this->renderEntities($entities, $type, $auth); |
||
358 | } |
||
359 | |||
360 | protected function artists($filter, $exact, $limit, $offset, $add, $update, $auth) { |
||
361 | $artists = $this->findEntities($this->artistBusinessLayer, $filter, $exact, $limit, $offset, $add, $update); |
||
362 | return $this->renderArtists($artists, $auth); |
||
363 | } |
||
364 | |||
365 | protected function artist($artistId, $auth) { |
||
366 | $userId = $this->ampacheUser->getUserId(); |
||
367 | $artist = $this->artistBusinessLayer->find($artistId, $userId); |
||
368 | return $this->renderArtists([$artist], $auth); |
||
369 | } |
||
370 | |||
371 | protected function artist_albums($artistId, $auth) { |
||
372 | $userId = $this->ampacheUser->getUserId(); |
||
373 | $albums = $this->albumBusinessLayer->findAllByArtist($artistId, $userId); |
||
374 | return $this->renderAlbums($albums, $auth); |
||
375 | } |
||
376 | |||
377 | protected function artist_songs($artistId, $auth) { |
||
378 | $userId = $this->ampacheUser->getUserId(); |
||
379 | $tracks = $this->trackBusinessLayer->findAllByArtist($artistId, $userId); |
||
380 | return $this->renderSongs($tracks, $auth); |
||
381 | } |
||
382 | |||
383 | protected function album_songs($albumId, $auth) { |
||
384 | $userId = $this->ampacheUser->getUserId(); |
||
385 | |||
386 | $album = $this->albumBusinessLayer->find($albumId, $userId); |
||
387 | $tracks = $this->trackBusinessLayer->findAllByAlbum($albumId, $userId); |
||
388 | |||
389 | foreach ($tracks as &$track) { |
||
390 | $track->setAlbum($album); |
||
391 | } |
||
392 | |||
393 | return $this->renderSongs($tracks, $auth); |
||
394 | } |
||
395 | |||
396 | protected function song($trackId, $auth) { |
||
397 | $userId = $this->ampacheUser->getUserId(); |
||
398 | $track = $this->trackBusinessLayer->find($trackId, $userId); |
||
399 | $trackInArray = [$track]; |
||
400 | return $this->renderSongs($trackInArray, $auth); |
||
401 | } |
||
402 | |||
403 | protected function songs($filter, $exact, $limit, $offset, $add, $update, $auth) { |
||
404 | |||
405 | // optimized handling for fetching the whole library |
||
406 | // note: the ordering of the songs differs between these two cases |
||
407 | if (empty($filter) && !$limit && !$offset && empty($add) && empty($update)) { |
||
408 | $tracks = $this->getAllTracks(); |
||
409 | } |
||
410 | // general case |
||
411 | else { |
||
412 | $tracks = $this->findEntities($this->trackBusinessLayer, $filter, $exact, $limit, $offset, $add, $update); |
||
413 | } |
||
414 | |||
415 | return $this->renderSongs($tracks, $auth); |
||
416 | } |
||
417 | |||
418 | protected function search_songs($filter, $auth) { |
||
419 | $userId = $this->ampacheUser->getUserId(); |
||
420 | $tracks = $this->trackBusinessLayer->findAllByNameRecursive($filter, $userId); |
||
421 | return $this->renderSongs($tracks, $auth); |
||
422 | } |
||
423 | |||
424 | protected function albums($filter, $exact, $limit, $offset, $add, $update, $auth) { |
||
425 | $albums = $this->findEntities($this->albumBusinessLayer, $filter, $exact, $limit, $offset, $add, $update); |
||
426 | return $this->renderAlbums($albums, $auth); |
||
427 | } |
||
428 | |||
429 | protected function album($albumId, $auth) { |
||
430 | $userId = $this->ampacheUser->getUserId(); |
||
431 | $album = $this->albumBusinessLayer->find($albumId, $userId); |
||
432 | return $this->renderAlbums([$album], $auth); |
||
433 | } |
||
434 | |||
435 | protected function playlists($filter, $exact, $limit, $offset, $add, $update) { |
||
436 | $userId = $this->ampacheUser->getUserId(); |
||
437 | $playlists = $this->findEntities($this->playlistBusinessLayer, $filter, $exact, $limit, $offset, $add, $update); |
||
438 | |||
439 | // append "All tracks" if not searching by name, and it is not off-limit |
||
440 | $allTracksIndex = $this->playlistBusinessLayer->count($userId); |
||
441 | if (empty($filter) && empty($add) && empty($update) |
||
442 | && self::indexIsWithinOffsetAndLimit($allTracksIndex, $offset, $limit)) { |
||
443 | $playlists[] = new AmpacheController_AllTracksPlaylist($userId, $this->trackBusinessLayer, $this->l10n); |
||
444 | } |
||
445 | |||
446 | return $this->renderPlaylists($playlists); |
||
447 | } |
||
448 | |||
449 | protected function playlist($listId) { |
||
450 | $userId = $this->ampacheUser->getUserId(); |
||
451 | if ($listId == self::ALL_TRACKS_PLAYLIST_ID) { |
||
452 | $playlist = new AmpacheController_AllTracksPlaylist($userId, $this->trackBusinessLayer, $this->l10n); |
||
453 | } else { |
||
454 | $playlist = $this->playlistBusinessLayer->find($listId, $userId); |
||
455 | } |
||
456 | return $this->renderPlaylists([$playlist]); |
||
457 | } |
||
458 | |||
459 | protected function playlist_songs($listId, $limit, $offset, $auth) { |
||
460 | if ($listId == self::ALL_TRACKS_PLAYLIST_ID) { |
||
461 | $playlistTracks = $this->getAllTracks(); |
||
462 | $playlistTracks = \array_slice($playlistTracks, $offset ?? 0, $limit); |
||
463 | } else { |
||
464 | $userId = $this->ampacheUser->getUserId(); |
||
465 | $playlistTracks = $this->playlistBusinessLayer->getPlaylistTracks($listId, $userId, $limit, $offset); |
||
466 | } |
||
467 | return $this->renderSongs($playlistTracks, $auth); |
||
468 | } |
||
469 | |||
470 | protected function playlist_create() { |
||
471 | $name = $this->getRequiredParam('name'); |
||
472 | $playlist = $this->playlistBusinessLayer->create($name, $this->ampacheUser->getUserId()); |
||
473 | return $this->renderPlaylists([$playlist]); |
||
474 | } |
||
475 | |||
476 | protected function playlist_edit($listId) { |
||
477 | $name = $this->request->getParam('name'); |
||
478 | $items = $this->request->getParam('items'); // track IDs |
||
479 | $tracks = $this->request->getParam('tracks'); // 1-based indices of the tracks |
||
480 | |||
481 | $edited = false; |
||
482 | $userId = $this->ampacheUser->getUserId(); |
||
483 | $playlist = $this->playlistBusinessLayer->find($listId, $userId); |
||
484 | |||
485 | if (!empty($name)) { |
||
486 | $playlist->setName($name); |
||
487 | $edited = true; |
||
488 | } |
||
489 | |||
490 | $newTrackIds = Util::explode(',', $items); |
||
491 | $newTrackOrdinals = Util::explode(',', $tracks); |
||
492 | |||
493 | if (\count($newTrackIds) != \count($newTrackOrdinals)) { |
||
494 | throw new AmpacheException("Arguments 'items' and 'tracks' must contain equal amount of elements", 400); |
||
495 | } elseif (\count($newTrackIds) > 0) { |
||
496 | $trackIds = $playlist->getTrackIdsAsArray(); |
||
497 | |||
498 | for ($i = 0, $count = \count($newTrackIds); $i < $count; ++$i) { |
||
499 | $trackId = $newTrackIds[$i]; |
||
500 | if (!$this->trackBusinessLayer->exists($trackId, $userId)) { |
||
501 | throw new AmpacheException("Invalid song ID $trackId", 404); |
||
502 | } |
||
503 | $trackIds[$newTrackOrdinals[$i]-1] = $trackId; |
||
504 | } |
||
505 | |||
506 | $playlist->setTrackIdsFromArray($trackIds); |
||
507 | $edited = true; |
||
508 | } |
||
509 | |||
510 | if ($edited) { |
||
511 | $this->playlistBusinessLayer->update($playlist); |
||
512 | return $this->ampacheResponse(['success' => 'playlist changes saved']); |
||
513 | } else { |
||
514 | throw new AmpacheException('Nothing was changed', 400); |
||
515 | } |
||
516 | } |
||
517 | |||
518 | protected function playlist_delete($listId) { |
||
519 | $this->playlistBusinessLayer->delete($listId, $this->ampacheUser->getUserId()); |
||
520 | return $this->ampacheResponse(['success' => 'playlist deleted']); |
||
521 | } |
||
522 | |||
523 | protected function playlist_add_song($listId) { |
||
524 | $song = $this->getRequiredParam('song'); // track ID |
||
525 | $check = $this->request->getParam('check', false); |
||
526 | |||
527 | $userId = $this->ampacheUser->getUserId(); |
||
528 | if (!$this->trackBusinessLayer->exists($song, $userId)) { |
||
529 | throw new AmpacheException("Invalid song ID $song", 404); |
||
530 | } |
||
531 | |||
532 | $playlist = $this->playlistBusinessLayer->find($listId, $userId); |
||
533 | $trackIds = $playlist->getTrackIdsAsArray(); |
||
534 | |||
535 | if ($check && \in_array($song, $trackIds)) { |
||
536 | throw new AmpacheException("Can't add a duplicate item when check is enabled", 400); |
||
537 | } |
||
538 | |||
539 | $trackIds[] = $song; |
||
540 | $playlist->setTrackIdsFromArray($trackIds); |
||
541 | $this->playlistBusinessLayer->update($playlist); |
||
542 | return $this->ampacheResponse(['success' => 'song added to playlist']); |
||
543 | } |
||
544 | |||
545 | protected function playlist_remove_song($listId) { |
||
546 | $song = $this->request->getParam('song'); // track ID |
||
547 | $track = $this->request->getParam('track'); // 1-based index of the track |
||
548 | $clear = $this->request->getParam('clear'); // added in API v420000 but we support this already now |
||
549 | |||
550 | $playlist = $this->playlistBusinessLayer->find($listId, $this->ampacheUser->getUserId()); |
||
551 | |||
552 | if ((int)$clear === 1) { |
||
553 | $trackIds = []; |
||
554 | $message = 'all songs removed from playlist'; |
||
555 | } elseif ($song !== null) { |
||
556 | $trackIds = $playlist->getTrackIdsAsArray(); |
||
557 | if (!\in_array($song, $trackIds)) { |
||
558 | throw new AmpacheException("Song $song not found in playlist", 404); |
||
559 | } |
||
560 | $trackIds = Util::arrayDiff($trackIds, [$song]); |
||
561 | $message = 'song removed from playlist'; |
||
562 | } elseif ($track !== null) { |
||
563 | $trackIds = $playlist->getTrackIdsAsArray(); |
||
564 | if ($track < 1 || $track > \count($trackIds)) { |
||
565 | throw new AmpacheException("Track ordinal $track is out of bounds", 404); |
||
566 | } |
||
567 | unset($trackIds[$track-1]); |
||
568 | $message = 'song removed from playlist'; |
||
569 | } else { |
||
570 | throw new AmpacheException("One of the arguments 'clear', 'song', 'track' is required", 400); |
||
571 | } |
||
572 | |||
573 | $playlist->setTrackIdsFromArray($trackIds); |
||
574 | $this->playlistBusinessLayer->update($playlist); |
||
575 | return $this->ampacheResponse(['success' => $message]); |
||
576 | } |
||
577 | |||
578 | protected function playlist_generate($filter, $limit, $offset, $auth) { |
||
579 | $mode = $this->request->getParam('mode', 'random'); |
||
580 | $album = $this->request->getParam('album'); |
||
581 | $artist = $this->request->getParam('artist'); |
||
582 | $flag = $this->request->getParam('flag'); |
||
583 | $format = $this->request->getParam('format', 'song'); |
||
584 | |||
585 | $tracks = $this->findEntities($this->trackBusinessLayer, $filter, false); // $limit and $offset are applied later |
||
586 | |||
587 | // filter the found tracks according to the additional requirements |
||
588 | if ($album !== null) { |
||
589 | $tracks = \array_filter($tracks, function ($track) use ($album) { |
||
590 | return ($track->getAlbumId() == $album); |
||
591 | }); |
||
592 | } |
||
593 | if ($artist !== null) { |
||
594 | $tracks = \array_filter($tracks, function ($track) use ($artist) { |
||
595 | return ($track->getArtistId() == $artist); |
||
596 | }); |
||
597 | } |
||
598 | if ($flag == 1) { |
||
599 | $tracks = \array_filter($tracks, function ($track) { |
||
600 | return ($track->getStarred() !== null); |
||
601 | }); |
||
602 | } |
||
603 | // After filtering, there may be "holes" between the array indices. Reindex the array. |
||
604 | $tracks = \array_values($tracks); |
||
605 | |||
606 | // Arguments 'limit' and 'offset' are optional |
||
607 | $limit = $limit ?? \count($tracks); |
||
608 | $offset = $offset ?? 0; |
||
609 | |||
610 | if ($mode == 'random') { |
||
611 | $userId = $this->ampacheUser->getUserId(); |
||
612 | $indices = $this->random->getIndices(\count($tracks), $offset, $limit, $userId, 'ampache_playlist_generate'); |
||
613 | $tracks = Util::arrayMultiGet($tracks, $indices); |
||
614 | } else { // 'recent', 'forgotten', 'unplayed' |
||
615 | throw new AmpacheException("Mode '$mode' is not supported", 400); |
||
616 | } |
||
617 | |||
618 | switch ($format) { |
||
619 | case 'song': |
||
620 | return $this->renderSongs($tracks, $auth); |
||
621 | case 'index': |
||
622 | return $this->renderSongsIndex($tracks); |
||
623 | case 'id': |
||
624 | return $this->renderEntityIds($tracks); |
||
625 | default: |
||
626 | throw new AmpacheException("Format '$format' is not supported", 400); |
||
627 | } |
||
628 | } |
||
629 | |||
630 | protected function podcasts($filter, $exact, $limit, $offset) { |
||
631 | $channels = $this->findEntities($this->podcastChannelBusinessLayer, $filter, $exact, $limit, $offset); |
||
632 | |||
633 | if ($this->request->getParam('include') === 'episodes') { |
||
634 | $userId = $this->ampacheUser->getUserId(); |
||
635 | $actuallyLimited = ($limit !== null && $limit < $this->podcastChannelBusinessLayer->count($userId)); |
||
636 | $allChannelsIncluded = (!$filter && !$actuallyLimited && !$offset); |
||
637 | $this->podcastService->injectEpisodes($channels, $userId, $allChannelsIncluded); |
||
638 | } |
||
639 | |||
640 | return $this->renderPodcastChannels($channels); |
||
641 | } |
||
642 | |||
643 | protected function podcast(int $channelId) { |
||
644 | $userId = $this->ampacheUser->getUserId(); |
||
645 | $channel = $this->podcastChannelBusinessLayer->find($channelId, $userId); |
||
646 | |||
647 | if ($this->request->getParam('include') === 'episodes') { |
||
648 | $channel->setEpisodes($this->podcastEpisodeBusinessLayer->findAllByChannel($channelId, $userId)); |
||
649 | } |
||
650 | |||
651 | return $this->renderPodcastChannels([$channel]); |
||
652 | } |
||
653 | |||
654 | protected function podcast_create() { |
||
655 | $url = $this->getRequiredParam('url'); |
||
656 | $userId = $this->ampacheUser->getUserId(); |
||
657 | $result = $this->podcastService->subscribe($url, $userId); |
||
658 | |||
659 | switch ($result['status']) { |
||
660 | case PodcastService::STATUS_OK: |
||
661 | return $this->renderPodcastChannels([$result['channel']]); |
||
662 | case PodcastService::STATUS_INVALID_URL: |
||
663 | throw new AmpacheException("Invalid URL $url", 400); |
||
664 | case PodcastService::STATUS_INVALID_RSS: |
||
665 | throw new AmpacheException("The document at URL $url is not a valid podcast RSS feed", 400); |
||
666 | case PodcastService::STATUS_ALREADY_EXISTS: |
||
667 | throw new AmpacheException('User already has this podcast channel subscribed', 400); |
||
668 | default: |
||
669 | throw new AmpacheException("Unexpected status code {$result['status']}", 400); |
||
670 | } |
||
671 | } |
||
672 | |||
673 | protected function podcast_delete(int $channelId) { |
||
674 | $userId = $this->ampacheUser->getUserId(); |
||
675 | $status = $this->podcastService->unsubscribe($channelId, $userId); |
||
676 | |||
677 | switch ($status) { |
||
678 | case PodcastService::STATUS_OK: |
||
679 | return $this->ampacheResponse(['success' => 'podcast deleted']); |
||
680 | case PodcastService::STATUS_NOT_FOUND: |
||
681 | throw new AmpacheException('Channel to be deleted not found', 404); |
||
682 | default: |
||
683 | throw new AmpacheException("Unexpected status code $status", 400); |
||
684 | } |
||
685 | } |
||
686 | |||
687 | protected function podcast_episodes(int $channelId, $limit, $offset) { |
||
691 | } |
||
692 | |||
693 | protected function podcast_episode(int $episodeId) { |
||
694 | $userId = $this->ampacheUser->getUserId(); |
||
695 | $episode = $this->podcastEpisodeBusinessLayer->find($episodeId, $userId); |
||
696 | return $this->renderPodcastEpisodes([$episode]); |
||
697 | } |
||
698 | |||
699 | protected function update_podcast(int $channelId) { |
||
700 | $userId = $this->ampacheUser->getUserId(); |
||
701 | $result = $this->podcastService->updateChannel($channelId, $userId); |
||
702 | |||
703 | switch ($result['status']) { |
||
704 | case PodcastService::STATUS_OK: |
||
705 | $message = $result['updated'] ? 'channel was updated from the souce' : 'no changes found'; |
||
706 | return $this->ampacheResponse(['success' => $message]); |
||
707 | case PodcastService::STATUS_NOT_FOUND: |
||
708 | throw new AmpacheException('Channel to be updated not found', 404); |
||
709 | case PodcastService::STATUS_INVALID_URL: |
||
710 | throw new AmpacheException('failed to read from the channel URL', 400); |
||
711 | case PodcastService::STATUS_INVALID_RSS: |
||
712 | throw new AmpacheException('the document at the channel URL is not a valid podcast RSS feed', 400); |
||
713 | default: |
||
714 | throw new AmpacheException("Unexpected status code {$result['status']}", 400); |
||
715 | } |
||
716 | } |
||
717 | |||
718 | protected function tags($filter, $exact, $limit, $offset) { |
||
719 | $genres = $this->findEntities($this->genreBusinessLayer, $filter, $exact, $limit, $offset); |
||
720 | return $this->renderTags($genres); |
||
721 | } |
||
722 | |||
723 | protected function tag($tagId) { |
||
724 | $userId = $this->ampacheUser->getUserId(); |
||
725 | $genre = $this->genreBusinessLayer->find($tagId, $userId); |
||
726 | return $this->renderTags([$genre]); |
||
727 | } |
||
728 | |||
729 | protected function tag_artists($genreId, $limit, $offset, $auth) { |
||
733 | } |
||
734 | |||
735 | protected function tag_albums($genreId, $limit, $offset, $auth) { |
||
736 | $userId = $this->ampacheUser->getUserId(); |
||
737 | $albums = $this->albumBusinessLayer->findAllByGenre($genreId, $userId, $limit, $offset); |
||
738 | return $this->renderAlbums($albums, $auth); |
||
739 | } |
||
740 | |||
741 | protected function tag_songs($genreId, $limit, $offset, $auth) { |
||
742 | $userId = $this->ampacheUser->getUserId(); |
||
743 | $tracks = $this->trackBusinessLayer->findAllByGenre($genreId, $userId, $limit, $offset); |
||
744 | return $this->renderSongs($tracks, $auth); |
||
745 | } |
||
746 | |||
747 | protected function flag() { |
||
748 | $type = $this->getRequiredParam('type'); |
||
749 | $id = $this->getRequiredParam('id'); |
||
750 | $flag = $this->getRequiredParam('flag'); |
||
751 | $flag = \filter_var($flag, FILTER_VALIDATE_BOOLEAN); |
||
752 | |||
753 | if (!\in_array($type, ['song', 'album', 'artist'])) { |
||
754 | throw new AmpacheException("Unsupported type $type", 400); |
||
755 | } |
||
756 | |||
757 | $userId = $this->ampacheUser->getUserId(); |
||
758 | $businessLayer = $this->getBusinessLayer($type); |
||
759 | if ($flag) { |
||
760 | $modifiedCount = $businessLayer->setStarred([$id], $userId); |
||
761 | $message = "flag ADDED to $id"; |
||
762 | } else { |
||
763 | $modifiedCount = $businessLayer->unsetStarred([$id], $userId); |
||
764 | $message = "flag REMOVED from $id"; |
||
765 | } |
||
766 | |||
767 | if ($modifiedCount > 0) { |
||
768 | return $this->ampacheResponse(['success' => $message]); |
||
769 | } else { |
||
770 | throw new AmpacheException("The $type $id was not found", 404); |
||
771 | } |
||
772 | } |
||
773 | |||
774 | protected function download(int $trackId) { |
||
775 | $type = $this->request->getParam('type', 'song'); |
||
776 | $userId = $this->ampacheUser->getUserId(); |
||
777 | |||
778 | if ($type === 'song') { |
||
779 | try { |
||
780 | $track = $this->trackBusinessLayer->find($trackId, $userId); |
||
781 | } catch (BusinessLayerException $e) { |
||
782 | return new ErrorResponse(Http::STATUS_NOT_FOUND, $e->getMessage()); |
||
783 | } |
||
784 | |||
785 | $file = $this->rootFolder->getUserFolder($userId)->getById($track->getFileId())[0] ?? null; |
||
786 | |||
787 | if ($file instanceof \OCP\Files\File) { |
||
788 | return new FileStreamResponse($file); |
||
789 | } else { |
||
790 | return new ErrorResponse(Http::STATUS_NOT_FOUND); |
||
791 | } |
||
792 | } elseif ($type === 'podcast') { |
||
793 | $episode = $this->podcastEpisodeBusinessLayer->find($trackId, $userId); |
||
794 | return new RedirectResponse($episode->getStreamUrl()); |
||
795 | } else { |
||
796 | throw new AmpacheException("Unsupported type '$type'", 400); |
||
797 | } |
||
798 | } |
||
799 | |||
800 | protected function stream(int $trackId, $offset) { |
||
801 | // This is just a dummy implementation. We don't support transcoding or streaming |
||
802 | // from a time offset. |
||
803 | // All the other unsupported arguments are just ignored, but a request with an offset |
||
804 | // is responded with an error. This is becuase the client would probably work in an |
||
805 | // unexpected way if it thinks it's streaming from offset but actually it is streaming |
||
806 | // from the beginning of the file. Returning an error gives the client a chance to fallback |
||
807 | // to other methods of seeking. |
||
808 | if ($offset !== null) { |
||
809 | throw new AmpacheException('Streaming with time offset is not supported', 400); |
||
810 | } |
||
811 | |||
812 | return $this->download($trackId); |
||
813 | } |
||
814 | |||
815 | protected function get_art(int $id) { |
||
816 | $type = $this->getRequiredParam('type'); |
||
817 | |||
818 | if (!\in_array($type, ['song', 'album', 'artist'])) { |
||
819 | throw new AmpacheException("Unsupported type $type", 400); |
||
820 | } |
||
821 | |||
822 | if ($type === 'song') { |
||
823 | // map song to its parent album |
||
824 | $id = $this->trackBusinessLayer->find($id, $this->ampacheUser->getUserId())->getAlbumId(); |
||
825 | $type = 'album'; |
||
826 | } |
||
827 | |||
828 | return $this->getCover($id, $this->getBusinessLayer($type)); |
||
829 | } |
||
830 | |||
831 | /******************** |
||
832 | * Helper functions * |
||
833 | ********************/ |
||
834 | |||
835 | private function getBusinessLayer($type) { |
||
836 | switch ($type) { |
||
837 | case 'song': return $this->trackBusinessLayer; |
||
838 | case 'album': return $this->albumBusinessLayer; |
||
839 | case 'artist': return $this->artistBusinessLayer; |
||
840 | case 'playlist': return $this->playlistBusinessLayer; |
||
841 | case 'podcast': return $this->podcastChannelBusinessLayer; |
||
842 | case 'podcast_episode': return $this->podcastEpisodeBusinessLayer; |
||
843 | case 'tag': return $this->genreBusinessLayer; |
||
844 | default: throw new AmpacheException("Unsupported type $type", 400); |
||
845 | } |
||
846 | } |
||
847 | |||
848 | private function renderEntities($entities, $type, $auth) { |
||
849 | switch ($type) { |
||
850 | case 'song': return $this->renderSongs($entities, $auth); |
||
851 | case 'album': return $this->renderAlbums($entities, $auth); |
||
852 | case 'artist': return $this->renderArtists($entities, $auth); |
||
853 | case 'playlist': return $this->renderPlaylists($entities); |
||
854 | case 'podcast': return $this->renderPodcastChannels($entities); |
||
855 | case 'podcast_episode': return $this->renderPodcastEpisodes($entities); |
||
856 | case 'tag': return $this->renderTags($entities); |
||
857 | default: throw new AmpacheException("Unsupported type $type", 400); |
||
858 | } |
||
859 | } |
||
860 | |||
861 | private function renderEntitiesIndex($entities, $type) { |
||
862 | switch ($type) { |
||
863 | case 'song': return $this->renderSongsIndex($entities); |
||
864 | case 'album': return $this->renderAlbumsIndex($entities); |
||
865 | case 'artist': return $this->renderArtistsIndex($entities); |
||
866 | case 'playlist': return $this->renderPlaylistsIndex($entities); |
||
867 | case 'podcast': return $this->renderPodcastChannelsIndex($entities); |
||
868 | case 'podcast_episode': return $this->renderPodcastEpisodesIndex($entities); |
||
869 | default: throw new AmpacheException("Unsupported type $type", 400); |
||
870 | } |
||
871 | } |
||
872 | |||
873 | private function getAppNameAndVersion() { |
||
874 | $vendor = 'owncloud/nextcloud'; // this should get overridden by the next 'include' |
||
875 | include \OC::$SERVERROOT . '/version.php'; |
||
876 | |||
877 | // Note: the following is deprecated since NC14 but the replacement |
||
878 | // \OCP\App\IAppManager::getAppVersion is not available before NC14. |
||
879 | $appVersion = \OCP\App::getAppVersion($this->appName); |
||
880 | |||
881 | return "$vendor {$this->appName} $appVersion"; |
||
882 | } |
||
883 | |||
884 | private function getCover(int $entityId, BusinessLayer $businessLayer) { |
||
885 | $userId = $this->ampacheUser->getUserId(); |
||
886 | $userFolder = $this->rootFolder->getUserFolder($userId); |
||
887 | |||
888 | try { |
||
889 | $entity = $businessLayer->find($entityId, $userId); |
||
890 | $coverData = $this->coverHelper->getCover($entity, $userId, $userFolder); |
||
891 | if ($coverData !== null) { |
||
892 | return new FileResponse($coverData); |
||
893 | } |
||
894 | } catch (BusinessLayerException $e) { |
||
895 | return new ErrorResponse(Http::STATUS_NOT_FOUND, 'entity not found'); |
||
896 | } |
||
897 | |||
898 | return new ErrorResponse(Http::STATUS_NOT_FOUND, 'entity has no cover'); |
||
899 | } |
||
900 | |||
901 | private function checkHandshakeTimestamp($timestamp, $currentTime) { |
||
902 | $providedTime = \intval($timestamp); |
||
903 | |||
904 | if ($providedTime === 0) { |
||
905 | throw new AmpacheException('Invalid Login - cannot parse time', 401); |
||
906 | } |
||
907 | if ($providedTime < ($currentTime - self::SESSION_EXPIRY_TIME)) { |
||
908 | throw new AmpacheException('Invalid Login - session is outdated', 401); |
||
909 | } |
||
910 | // Allow the timestamp to be at maximum 10 minutes in the future. The client may use its |
||
911 | // own system clock to generate the timestamp and that may differ from the server's time. |
||
912 | if ($providedTime > $currentTime + 600) { |
||
913 | throw new AmpacheException('Invalid Login - timestamp is in future', 401); |
||
914 | } |
||
915 | } |
||
916 | |||
917 | private function checkHandshakeAuthentication($user, $timestamp, $auth) { |
||
918 | $hashes = $this->ampacheUserMapper->getPasswordHashes($user); |
||
919 | |||
920 | foreach ($hashes as $hash) { |
||
921 | $expectedHash = \hash('sha256', $timestamp . $hash); |
||
922 | |||
923 | if ($expectedHash === $auth) { |
||
924 | return; |
||
925 | } |
||
926 | } |
||
927 | |||
928 | throw new AmpacheException('Invalid Login - passphrase does not match', 401); |
||
929 | } |
||
930 | |||
931 | private function startNewSession($user, $expiryDate) { |
||
932 | $token = Random::secure(16); |
||
933 | |||
934 | // create new session |
||
935 | $session = new AmpacheSession(); |
||
936 | $session->setUserId($user); |
||
937 | $session->setToken($token); |
||
938 | $session->setExpiry($expiryDate); |
||
939 | |||
940 | // save session |
||
941 | $this->ampacheSessionMapper->insert($session); |
||
942 | |||
943 | return $token; |
||
944 | } |
||
945 | |||
946 | private function findEntities( |
||
947 | BusinessLayer $businessLayer, $filter, $exact, $limit=null, $offset=null, $add=null, $update=null) : array { |
||
948 | |||
949 | $userId = $this->ampacheUser->getUserId(); |
||
950 | |||
951 | // It's not documented, but Ampache supports also specifying date range on `add` and `update` parameters |
||
952 | // by using '/' as separator. If there is no such separator, then the value is used as a lower limit. |
||
953 | $add = Util::explode('/', $add); |
||
954 | $update = Util::explode('/', $update); |
||
955 | $addMin = $add[0] ?? null; |
||
956 | $addMax = $add[1] ?? null; |
||
957 | $updateMin = $update[0] ?? null; |
||
958 | $updateMax = $update[1] ?? null; |
||
959 | |||
960 | if ($filter) { |
||
961 | $fuzzy = !((boolean) $exact); |
||
962 | return $businessLayer->findAllByName($filter, $userId, $fuzzy, $limit, $offset, $addMin, $addMax, $updateMin, $updateMax); |
||
963 | } else { |
||
964 | return $businessLayer->findAll($userId, SortBy::Name, $limit, $offset, $addMin, $addMax, $updateMin, $updateMax); |
||
965 | } |
||
966 | } |
||
967 | |||
968 | /** |
||
969 | * Getting all tracks with this helper is more efficient than with `findEntities` |
||
970 | * followed by a call to `albumBusinessLayer->find(...)` on each track. |
||
971 | * This is because, under the hood, the albums are fetched with a single DB query |
||
972 | * instead of fetching each separately. |
||
973 | * |
||
974 | * The result set is ordered first by artist and then by song title. |
||
975 | */ |
||
976 | private function getAllTracks() { |
||
977 | $userId = $this->ampacheUser->getUserId(); |
||
978 | $tracks = $this->library->getTracksAlbumsAndArtists($userId)['tracks']; |
||
979 | \usort($tracks, ['\OCA\Music\Db\Track', 'compareArtistAndTitle']); |
||
980 | foreach ($tracks as $index => &$track) { |
||
981 | $track->setNumberOnPlaylist($index + 1); |
||
982 | } |
||
983 | return $tracks; |
||
984 | } |
||
985 | |||
986 | private function createAmpacheActionUrl($action, $id, $auth, $type=null) { |
||
987 | $api = $this->jsonMode ? 'music.ampache.jsonApi' : 'music.ampache.xmlApi'; |
||
988 | return $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute($api)) |
||
989 | . "?action=$action&id=$id&auth=$auth" |
||
990 | . (!empty($type) ? "&type=$type" : ''); |
||
991 | } |
||
992 | |||
993 | private function createCoverUrl($entity, $auth) { |
||
994 | if ($entity instanceof Album) { |
||
995 | $type = 'album'; |
||
996 | } elseif ($entity instanceof Artist) { |
||
997 | $type = 'artist'; |
||
998 | } else { |
||
999 | throw new AmpacheException('unexpeted entity type for cover image', 500); |
||
1000 | } |
||
1001 | |||
1002 | if ($entity->getCoverFileId()) { |
||
1003 | return $this->createAmpacheActionUrl("get_art", $entity->getId(), $auth, $type); |
||
1004 | } else { |
||
1005 | return ''; |
||
1006 | } |
||
1007 | } |
||
1008 | |||
1009 | /** |
||
1010 | * Any non-integer values and integer value 0 are converted to null to |
||
1011 | * indicate "no limit" or "no offset". |
||
1012 | * @param string $value |
||
1013 | * @return integer|null |
||
1014 | */ |
||
1015 | private static function validateLimitOrOffset($value) : ?int { |
||
1016 | $value = (int)$value; |
||
1017 | return ($value > 0) ? $value : null; |
||
1018 | } |
||
1019 | |||
1020 | /** |
||
1021 | * @param int $index |
||
1022 | * @param int|null $offset |
||
1023 | * @param int|null $limit |
||
1024 | * @return boolean |
||
1025 | */ |
||
1026 | private static function indexIsWithinOffsetAndLimit($index, $offset, $limit) { |
||
1027 | $offset = \intval($offset); // missing offset is interpreted as 0-offset |
||
1028 | return ($limit === null) || ($index >= $offset && $index < $offset + $limit); |
||
1029 | } |
||
1030 | |||
1031 | private function renderArtists($artists, $auth) { |
||
1032 | $userId = $this->ampacheUser->getUserId(); |
||
1033 | $genreMap = Util::createIdLookupTable($this->genreBusinessLayer->findAll($userId)); |
||
1034 | |||
1035 | return $this->ampacheResponse([ |
||
1036 | 'artist' => \array_map(function ($artist) use ($userId, $genreMap, $auth) { |
||
1037 | return [ |
||
1038 | 'id' => (string)$artist->getId(), |
||
1039 | 'name' => $artist->getNameString($this->l10n), |
||
1040 | 'albums' => $this->albumBusinessLayer->countByArtist($artist->getId()), |
||
1041 | 'songs' => $this->trackBusinessLayer->countByArtist($artist->getId()), |
||
1042 | 'art' => $this->createCoverUrl($artist, $auth), |
||
1043 | 'rating' => 0, |
||
1044 | 'preciserating' => 0, |
||
1045 | 'tag' => \array_map(function ($genreId) use ($genreMap) { |
||
1046 | return [ |
||
1047 | 'id' => (string)$genreId, |
||
1048 | 'value' => $genreMap[$genreId]->getNameString($this->l10n), |
||
1049 | 'count' => 1 |
||
1050 | ]; |
||
1051 | }, $this->trackBusinessLayer->getGenresByArtistId($artist->getId(), $userId)) |
||
1052 | ]; |
||
1053 | }, $artists) |
||
1054 | ]); |
||
1055 | } |
||
1056 | |||
1057 | private function renderAlbums($albums, $auth) { |
||
1058 | return $this->ampacheResponse([ |
||
1059 | 'album' => \array_map(function ($album) use ($auth) { |
||
1060 | return [ |
||
1061 | 'id' => (string)$album->getId(), |
||
1062 | 'name' => $album->getNameString($this->l10n), |
||
1063 | 'artist' => [ |
||
1064 | 'id' => (string)$album->getAlbumArtistId(), |
||
1065 | 'value' => $album->getAlbumArtistNameString($this->l10n) |
||
1066 | ], |
||
1067 | 'tracks' => $this->trackBusinessLayer->countByAlbum($album->getId()), |
||
1068 | 'rating' => 0, |
||
1069 | 'year' => $album->yearToAPI(), |
||
1070 | 'art' => $this->createCoverUrl($album, $auth), |
||
1071 | 'preciserating' => 0, |
||
1072 | 'tag' => \array_map(function ($genre) { |
||
1073 | return [ |
||
1074 | 'id' => (string)$genre->getId(), |
||
1075 | 'value' => $genre->getNameString($this->l10n), |
||
1076 | 'count' => 1 |
||
1077 | ]; |
||
1078 | }, $album->getGenres()) |
||
1079 | ]; |
||
1080 | }, $albums) |
||
1081 | ]); |
||
1082 | } |
||
1083 | |||
1084 | private function renderSongs($tracks, $auth) { |
||
1085 | return $this->ampacheResponse([ |
||
1086 | 'song' => \array_map(function ($track) use ($auth) { |
||
1087 | $userId = $this->ampacheUser->getUserId(); |
||
1088 | $album = $track->getAlbum() |
||
1089 | ?: $this->albumBusinessLayer->findOrDefault($track->getAlbumId(), $userId); |
||
1090 | |||
1091 | $result = [ |
||
1092 | 'id' => (string)$track->getId(), |
||
1093 | 'title' => $track->getTitle() ?: '', |
||
1094 | 'name' => $track->getTitle() ?: '', |
||
1095 | 'artist' => [ |
||
1096 | 'id' => (string)$track->getArtistId() ?: '0', |
||
1097 | 'value' => $track->getArtistNameString($this->l10n) |
||
1098 | ], |
||
1099 | 'albumartist' => [ |
||
1100 | 'id' => (string)$album->getAlbumArtistId() ?: '0', |
||
1101 | 'value' => $album->getAlbumArtistNameString($this->l10n) |
||
1102 | ], |
||
1103 | 'album' => [ |
||
1104 | 'id' => (string)$album->getId() ?: '0', |
||
1105 | 'value' => $album->getNameString($this->l10n) |
||
1106 | ], |
||
1107 | 'url' => $this->createAmpacheActionUrl('download', $track->getId(), $auth), |
||
1108 | 'time' => $track->getLength(), |
||
1109 | 'year' => $track->getYear(), |
||
1110 | 'track' => $track->getAdjustedTrackNumber(), |
||
1111 | 'bitrate' => $track->getBitrate(), |
||
1112 | 'mime' => $track->getMimetype(), |
||
1113 | 'size' => $track->getSize(), |
||
1114 | 'art' => $this->createCoverUrl($album, $auth), |
||
1115 | 'rating' => 0, |
||
1116 | 'preciserating' => 0, |
||
1117 | ]; |
||
1118 | |||
1119 | $genreId = $track->getGenreId(); |
||
1120 | if ($genreId !== null) { |
||
1121 | $result['tag'] = [[ |
||
1122 | 'id' => (string)$genreId, |
||
1123 | 'value' => $track->getGenreNameString($this->l10n), |
||
1124 | 'count' => 1 |
||
1125 | ]]; |
||
1126 | } |
||
1127 | return $result; |
||
1128 | }, $tracks) |
||
1129 | ]); |
||
1130 | } |
||
1131 | |||
1132 | private function renderPlaylists($playlists) { |
||
1133 | return $this->ampacheResponse([ |
||
1134 | 'playlist' => \array_map(function ($playlist) { |
||
1135 | return [ |
||
1136 | 'id' => (string)$playlist->getId(), |
||
1137 | 'name' => $playlist->getName(), |
||
1138 | 'owner' => $this->ampacheUser->getUserId(), |
||
1139 | 'items' => $playlist->getTrackCount(), |
||
1140 | 'type' => 'Private' |
||
1141 | ]; |
||
1142 | }, $playlists) |
||
1143 | ]); |
||
1144 | } |
||
1145 | |||
1146 | private function renderPodcastChannels(array $channels) { |
||
1147 | return $this->ampacheResponse([ |
||
1148 | 'podcast' => Util::arrayMapMethod($channels, 'toAmpacheApi') |
||
1149 | ]); |
||
1150 | } |
||
1151 | |||
1152 | private function renderPodcastEpisodes(array $episodes) { |
||
1155 | ]); |
||
1156 | } |
||
1157 | |||
1158 | private function renderTags($genres) { |
||
1159 | return $this->ampacheResponse([ |
||
1160 | 'tag' => \array_map(function ($genre) { |
||
1161 | return [ |
||
1162 | 'id' => (string)$genre->getId(), |
||
1163 | 'name' => $genre->getNameString($this->l10n), |
||
1164 | 'albums' => $genre->getAlbumCount(), |
||
1165 | 'artists' => $genre->getArtistCount(), |
||
1166 | 'songs' => $genre->getTrackCount(), |
||
1167 | 'videos' => 0, |
||
1168 | 'playlists' => 0, |
||
1169 | 'stream' => 0 |
||
1170 | ]; |
||
1171 | }, $genres) |
||
1172 | ]); |
||
1173 | } |
||
1174 | |||
1175 | private function renderSongsIndex($tracks) { |
||
1176 | return $this->ampacheResponse([ |
||
1177 | 'song' => \array_map(function ($track) { |
||
1178 | return [ |
||
1179 | 'id' => (string)$track->getId(), |
||
1180 | 'title' => $track->getTitle(), |
||
1181 | 'name' => $track->getTitle(), |
||
1182 | 'artist' => [ |
||
1183 | 'id' => (string)$track->getArtistId(), |
||
1184 | 'value' => $track->getArtistNameString($this->l10n) |
||
1185 | ], |
||
1186 | 'album' => [ |
||
1187 | 'id' => (string)$track->getAlbumId(), |
||
1188 | 'value' => $track->getAlbumNameString($this->l10n) |
||
1189 | ] |
||
1190 | ]; |
||
1191 | }, $tracks) |
||
1192 | ]); |
||
1193 | } |
||
1194 | |||
1195 | private function renderAlbumsIndex($albums) { |
||
1196 | return $this->ampacheResponse([ |
||
1197 | 'album' => \array_map(function ($album) { |
||
1198 | return [ |
||
1199 | 'id' => (string)$album->getId(), |
||
1200 | 'name' => $album->getNameString($this->l10n), |
||
1201 | 'artist' => [ |
||
1202 | 'id' => (string)$album->getAlbumArtistId(), |
||
1203 | 'value' => $album->getAlbumArtistNameString($this->l10n) |
||
1204 | ] |
||
1205 | ]; |
||
1206 | }, $albums) |
||
1207 | ]); |
||
1208 | } |
||
1209 | |||
1210 | private function renderArtistsIndex($artists) { |
||
1211 | return $this->ampacheResponse([ |
||
1212 | 'artist' => \array_map(function ($artist) { |
||
1213 | $userId = $this->ampacheUser->getUserId(); |
||
1214 | $albums = $this->albumBusinessLayer->findAllByArtist($artist->getId(), $userId); |
||
1215 | |||
1216 | return [ |
||
1217 | 'id' => (string)$artist->getId(), |
||
1218 | 'name' => $artist->getNameString($this->l10n), |
||
1219 | 'album' => \array_map(function ($album) { |
||
1220 | return [ |
||
1221 | 'id' => (string)$album->getId(), |
||
1222 | 'value' => $album->getNameString($this->l10n) |
||
1223 | ]; |
||
1224 | }, $albums) |
||
1225 | ]; |
||
1226 | }, $artists) |
||
1227 | ]); |
||
1228 | } |
||
1229 | |||
1230 | private function renderPlaylistsIndex($playlists) { |
||
1231 | return $this->ampacheResponse([ |
||
1232 | 'playlist' => \array_map(function ($playlist) { |
||
1233 | return [ |
||
1234 | 'id' => (string)$playlist->getId(), |
||
1235 | 'name' => $playlist->getName(), |
||
1236 | 'playlisttrack' => $playlist->getTrackIdsAsArray() |
||
1237 | ]; |
||
1238 | }, $playlists) |
||
1239 | ]); |
||
1240 | } |
||
1241 | |||
1242 | private function renderPodcastChannelsIndex(array $channels) { |
||
1245 | } |
||
1246 | |||
1247 | private function renderPodcastEpisodesIndex(array $episodes) { |
||
1248 | // The v4 API spec does not give any examples of this, and the v5 example is almost identical to the v4 "normal" result |
||
1249 | return $this->renderPodcastEpisodes($episodes); |
||
1250 | } |
||
1251 | |||
1252 | private function renderEntityIds($entities) { |
||
1253 | return $this->ampacheResponse(['id' => Util::extractIds($entities)]); |
||
1254 | } |
||
1255 | |||
1256 | /** |
||
1257 | * Array is considered to be "indexed" if its first element has numerical key. |
||
1258 | * Empty array is considered to be "indexed". |
||
1259 | * @param array $array |
||
1260 | */ |
||
1261 | private static function arrayIsIndexed(array $array) { |
||
1262 | \reset($array); |
||
1263 | return empty($array) || \is_int(\key($array)); |
||
1264 | } |
||
1265 | |||
1266 | /** |
||
1267 | * The JSON API has some asymmetries with the XML API. This function makes the needed |
||
1268 | * translations for the result content before it is converted into JSON. |
||
1269 | * @param array $content |
||
1270 | * @return array |
||
1271 | */ |
||
1272 | private static function prepareResultForJsonApi($content) { |
||
1273 | // In all responses returning an array of library entities, the root node is anonymous. |
||
1274 | // Unwrap the outermost array if it is an associative array with a single array-type value. |
||
1275 | if (\count($content) === 1 && !self::arrayIsIndexed($content) |
||
1276 | && \is_array(\current($content)) && self::arrayIsIndexed(\current($content))) { |
||
1277 | $content = \array_pop($content); |
||
1278 | } |
||
1279 | |||
1280 | // The key 'value' has a special meaning on XML responses, as it makes the corresponding value |
||
1281 | // to be treated as text content of the parent element. In the JSON API, these are mostly |
||
1282 | // substituted with property 'name', but error responses use the property 'message', instead. |
||
1283 | if (\array_key_exists('error', $content)) { |
||
1284 | $content = Util::convertArrayKeys($content, ['value' => 'message']); |
||
1285 | } else { |
||
1286 | $content = Util::convertArrayKeys($content, ['value' => 'name']); |
||
1287 | } |
||
1288 | return $content; |
||
1289 | } |
||
1290 | |||
1291 | /** |
||
1292 | * The XML API has some asymmetries with the JSON API. This function makes the needed |
||
1293 | * translations for the result content before it is converted into XML. |
||
1294 | * @param array $content |
||
1295 | * @return array |
||
1296 | */ |
||
1297 | private static function prepareResultForXmlApi($content) { |
||
1315 | } |
||
1316 | |||
1317 | private function getRequiredParam($paramName) { |
||
1318 | $param = $this->request->getParam($paramName); |
||
1319 | |||
1320 | if ($param === null) { |
||
1325 | } |
||
1326 | } |
||
1327 | |||
1328 | /** |
||
1329 | * Adapter class which acts like the Playlist class for the purpose of |
||
1330 | * AmpacheController::renderPlaylists but contains all the track of the user. |
||
1331 | */ |
||
1332 | class AmpacheController_AllTracksPlaylist { |
||
1333 | private $user; |
||
1334 | private $trackBusinessLayer; |
||
1335 | private $l10n; |
||
1336 | |||
1337 | public function __construct($user, $trackBusinessLayer, $l10n) { |
||
1338 | $this->user = $user; |
||
1339 | $this->trackBusinessLayer = $trackBusinessLayer; |
||
1340 | $this->l10n = $l10n; |
||
1341 | } |
||
1342 | |||
1343 | public function getId() { |
||
1344 | return AmpacheController::ALL_TRACKS_PLAYLIST_ID; |
||
1345 | } |
||
1346 | |||
1347 | public function getName() { |
||
1348 | return $this->l10n->t('All tracks'); |
||
1349 | } |
||
1350 | |||
1351 | public function getTrackCount() { |
||
1352 | return $this->trackBusinessLayer->count($this->user); |
||
1353 | } |
||
1354 | } |
||
1355 |