Passed
Push — master ( 3c5a6f...e94730 )
by Pauli
09:42 queued 11s
created

ApiController::prepareCollection()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 8
nc 2
nop 0
dl 0
loc 12
rs 10
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
/**
4
 * ownCloud - Music app
5
 *
6
 * This file is licensed under the Affero General Public License version 3 or
7
 * later. See the COPYING file.
8
 *
9
 * @author Morris Jobke <[email protected]>
10
 * @author Pauli Järvinen <[email protected]>
11
 * @copyright Morris Jobke 2013, 2014
12
 * @copyright Pauli Järvinen 2017 - 2020
13
 */
14
15
namespace OCA\Music\Controller;
16
17
use \OCP\AppFramework\Controller;
18
use \OCP\AppFramework\Http;
19
use \OCP\AppFramework\Http\DataDisplayResponse;
20
use \OCP\AppFramework\Http\JSONResponse;
21
use \OCP\AppFramework\Http\RedirectResponse;
22
use \OCP\AppFramework\Http\Response;
23
use \OCP\Files\Folder;
24
use \OCP\IL10N;
25
use \OCP\IRequest;
26
use \OCP\IURLGenerator;
27
28
use \OCA\Music\AppFramework\BusinessLayer\BusinessLayerException;
29
use \OCA\Music\AppFramework\Core\Logger;
30
use \OCA\Music\BusinessLayer\AlbumBusinessLayer;
31
use \OCA\Music\BusinessLayer\ArtistBusinessLayer;
32
use \OCA\Music\BusinessLayer\GenreBusinessLayer;
33
use \OCA\Music\BusinessLayer\TrackBusinessLayer;
34
use \OCA\Music\Db\Album;
35
use \OCA\Music\Db\Artist;
36
use \OCA\Music\Db\Maintenance;
37
use \OCA\Music\Db\Track;
38
use \OCA\Music\Http\ErrorResponse;
39
use \OCA\Music\Http\FileResponse;
40
use \OCA\Music\Http\FileStreamResponse;
41
use \OCA\Music\Utility\CollectionHelper;
42
use \OCA\Music\Utility\CoverHelper;
43
use \OCA\Music\Utility\DetailsHelper;
44
use \OCA\Music\Utility\LastfmService;
45
use \OCA\Music\Utility\Scanner;
46
use \OCA\Music\Utility\UserMusicFolder;
47
use \OCA\Music\Utility\Util;
48
49
class ApiController extends Controller {
50
51
	/** @var IL10N */
52
	private $l10n;
53
	/** @var TrackBusinessLayer */
54
	private $trackBusinessLayer;
55
	/** @var ArtistBusinessLayer */
56
	private $artistBusinessLayer;
57
	/** @var AlbumBusinessLayer */
58
	private $albumBusinessLayer;
59
	/** @var GenreBusinessLayer */
60
	private $genreBusinessLayer;
61
	/** @var Scanner */
62
	private $scanner;
63
	/** @var CollectionHelper */
64
	private $collectionHelper;
65
	/** @var CoverHelper */
66
	private $coverHelper;
67
	/** @var DetailsHelper */
68
	private $detailsHelper;
69
	/** @var LastfmService */
70
	private $lastfmService;
71
	/** @var Maintenance */
72
	private $maintenance;
73
	/** @var UserMusicFolder */
74
	private $userMusicFolder;
75
	/** @var string */
76
	private $userId;
77
	/** @var IURLGenerator */
78
	private $urlGenerator;
79
	/** @var Folder */
80
	private $userFolder;
81
	/** @var Logger */
82
	private $logger;
83
84
	public function __construct(string $appname,
85
								IRequest $request,
86
								IURLGenerator $urlGenerator,
87
								TrackBusinessLayer $trackbusinesslayer,
88
								ArtistBusinessLayer $artistbusinesslayer,
89
								AlbumBusinessLayer $albumbusinesslayer,
90
								GenreBusinessLayer $genreBusinessLayer,
91
								Scanner $scanner,
92
								CollectionHelper $collectionHelper,
93
								CoverHelper $coverHelper,
94
								DetailsHelper $detailsHelper,
95
								LastfmService $lastfmService,
96
								Maintenance $maintenance,
97
								UserMusicFolder $userMusicFolder,
98
								?string $userId, // null if this gets called after the user has logged out
99
								IL10N $l10n,
100
								?Folder $userFolder, // null if this gets called after the user has logged out
101
								Logger $logger) {
102
		parent::__construct($appname, $request);
103
		$this->l10n = $l10n;
104
		$this->trackBusinessLayer = $trackbusinesslayer;
105
		$this->artistBusinessLayer = $artistbusinesslayer;
106
		$this->albumBusinessLayer = $albumbusinesslayer;
107
		$this->genreBusinessLayer = $genreBusinessLayer;
108
		$this->scanner = $scanner;
109
		$this->collectionHelper = $collectionHelper;
110
		$this->coverHelper = $coverHelper;
111
		$this->detailsHelper = $detailsHelper;
112
		$this->lastfmService = $lastfmService;
113
		$this->maintenance = $maintenance;
114
		$this->userMusicFolder = $userMusicFolder;
115
		$this->userId = $userId;
116
		$this->urlGenerator = $urlGenerator;
117
		$this->userFolder = $userFolder;
118
		$this->logger = $logger;
119
	}
120
121
	/**
122
	 * Extracts the id from an unique slug (id-slug)
123
	 * @param string|int $slug the slug
124
	 * @return int the id
125
	 */
126
	protected static function getIdFromSlug($slug) : int {
127
		if (\is_string($slug)) {
128
			$split = \explode('-', $slug, 2);
129
			return (int)$split[0];
130
		} elseif (\is_int($slug)) {
0 ignored issues
show
introduced by
The condition is_int($slug) is always true.
Loading history...
131
			return $slug;
132
		} else {
133
			throw new \InvalidArgumentException();
134
		}
135
	}
136
137
	/**
138
	 * @NoAdminRequired
139
	 * @NoCSRFRequired
140
	 */
141
	public function prepareCollection() {
142
		$hash = $this->collectionHelper->getCachedJsonHash();
143
		if ($hash === null) {
144
			// build the collection but ignore the data for now
145
			$this->collectionHelper->getJson();
146
			$hash = $this->collectionHelper->getCachedJsonHash();
147
		}
148
		$coverToken = $this->coverHelper->createAccessToken($this->userId);
149
150
		return new JSONResponse([
151
			'hash' => $hash,
152
			'cover_token' => $coverToken
153
		]);
154
	}
155
156
	/**
157
	 * @NoAdminRequired
158
	 * @NoCSRFRequired
159
	 */
160
	public function collection() {
161
		$collectionJson = $this->collectionHelper->getJson();
162
		$response = new DataDisplayResponse($collectionJson);
163
		$response->addHeader('Content-Type', 'application/json; charset=utf-8');
164
165
		// Instruct the client to cache the result in case it requested the collection with
166
		// the correct hash. The hash could be incorrect if the collection would have changed
167
		// between calls to prepareCollection() and colletion().
168
		$requestHash = $this->request->getParam('hash');
169
		$actualHash = $this->collectionHelper->getCachedJsonHash();
170
		if (!empty($actualHash) && $requestHash === $actualHash) {
171
			self::setClientCaching($response, 90); // cache for 3 months
172
		}
173
174
		return $response;
175
	}
176
177
	/**
178
	 * @NoAdminRequired
179
	 * @NoCSRFRequired
180
	 */
181
	public function folders() {
182
		$musicFolder = $this->userMusicFolder->getFolder($this->userId);
183
		$folders = $this->trackBusinessLayer->findAllFolders($this->userId, $musicFolder);
184
		return new JSONResponse($folders);
185
	}
186
187
	/**
188
	 * @NoAdminRequired
189
	 * @NoCSRFRequired
190
	 */
191
	public function genres() {
192
		$genres = $this->genreBusinessLayer->findAllWithTrackIds($this->userId);
193
		$unscanned =  $this->trackBusinessLayer->findFilesWithoutScannedGenre($this->userId);
194
		return new JSONResponse([
195
			'genres' => \array_map(function ($g) {
196
				return $g->toApi();
197
			}, $genres),
198
			'unscanned' => $unscanned
199
		]);
200
	}
201
202
	/**
203
	 * @NoAdminRequired
204
	 * @NoCSRFRequired
205
	 */
206
	public function artists($fulltree, $albums) {
207
		$fulltree = \filter_var($fulltree, FILTER_VALIDATE_BOOLEAN);
208
		$includeAlbums = \filter_var($albums, FILTER_VALIDATE_BOOLEAN);
209
		/** @var Artist[] $artists */
210
		$artists = $this->artistBusinessLayer->findAll($this->userId);
211
212
		$artists = \array_map(function ($a) use ($fulltree, $includeAlbums) {
213
			return $this->artistToApi($a, $includeAlbums || $fulltree, $fulltree);
214
		}, $artists);
215
216
		return new JSONResponse($artists);
217
	}
218
219
	/**
220
	 * @NoAdminRequired
221
	 * @NoCSRFRequired
222
	 */
223
	public function artist($artistIdOrSlug, $fulltree) {
224
		$fulltree = \filter_var($fulltree, FILTER_VALIDATE_BOOLEAN);
225
		$artistId = $this->getIdFromSlug($artistIdOrSlug);
226
		/** @var Artist $artist */
227
		$artist = $this->artistBusinessLayer->find($artistId, $this->userId);
228
		$artist = $this->artistToApi($artist, $fulltree, $fulltree);
229
		return new JSONResponse($artist);
230
	}
231
232
	/**
233
	 * Return given artist in Shia API format
234
	 * @param Artist $artist
235
	 * @param boolean $includeAlbums
236
	 * @param boolean $includeTracks (ignored if $includeAlbums==false)
237
	 * @return array
238
	 */
239
	private function artistToApi(Artist $artist, bool $includeAlbums, bool $includeTracks) : array {
240
		$artistInApi = $artist->toAPI($this->urlGenerator, $this->l10n);
241
		if ($includeAlbums) {
242
			$artistId = $artist->getId();
243
			$albums = $this->albumBusinessLayer->findAllByArtist($artistId, $this->userId);
244
245
			$artistInApi['albums'] = \array_map(function ($a) use ($includeTracks) {
246
				return $this->albumToApi($a, $includeTracks, false);
247
			}, $albums);
248
		}
249
		return $artistInApi;
250
	}
251
252
	/**
253
	 * @NoAdminRequired
254
	 * @NoCSRFRequired
255
	 */
256
	public function albums(?int $artist, $fulltree) {
257
		$fulltree = \filter_var($fulltree, FILTER_VALIDATE_BOOLEAN);
258
		if ($artist !== null) {
259
			$albums = $this->albumBusinessLayer->findAllByArtist($artist, $this->userId);
260
		} else {
261
			$albums = $this->albumBusinessLayer->findAll($this->userId);
262
		}
263
264
		$albums = \array_map(function ($a) use ($fulltree) {
265
			return $this->albumToApi($a, $fulltree, $fulltree);
266
		}, $albums);
267
268
		return new JSONResponse($albums);
269
	}
270
271
	/**
272
	 * @NoAdminRequired
273
	 * @NoCSRFRequired
274
	 */
275
	public function album($albumIdOrSlug, $fulltree) {
276
		$fulltree = \filter_var($fulltree, FILTER_VALIDATE_BOOLEAN);
277
		$albumId = $this->getIdFromSlug($albumIdOrSlug);
278
		$album = $this->albumBusinessLayer->find($albumId, $this->userId);
279
		$album = $this->albumToApi($album, $fulltree, $fulltree);
280
		return new JSONResponse($album);
281
	}
282
283
	/**
284
	 * Return given album in the Shiva API format
285
	 */
286
	private function albumToApi(Album $album, bool $includeTracks, bool $includeArtists) : array {
287
		$albumInApi = $album->toAPI($this->urlGenerator, $this->l10n);
288
289
		if ($includeTracks) {
290
			$albumId = $album->getId();
291
			$tracks = $this->trackBusinessLayer->findAllByAlbum($albumId, $this->userId);
292
			$albumInApi['tracks'] = \array_map(function ($t) {
293
				return $t->toAPI($this->urlGenerator);
294
			}, $tracks);
295
		}
296
297
		if ($includeArtists) {
298
			$artistIds = $album->getArtistIds();
299
			$artists = $this->artistBusinessLayer->findById($artistIds, $this->userId);
300
			$albumInApi['artists'] = \array_map(function ($a) {
301
				return $a->toAPI($this->urlGenerator, $this->l10n);
302
			}, $artists);
303
		}
304
305
		return $albumInApi;
306
	}
307
308
	/**
309
	 * @NoAdminRequired
310
	 * @NoCSRFRequired
311
	 */
312
	public function tracks($artist, $album, $fulltree) {
313
		$fulltree = \filter_var($fulltree, FILTER_VALIDATE_BOOLEAN);
314
		if ($artist) {
315
			$tracks = $this->trackBusinessLayer->findAllByArtist($artist, $this->userId);
316
		} elseif ($album) {
317
			$tracks = $this->trackBusinessLayer->findAllByAlbum($album, $this->userId);
318
		} else {
319
			$tracks = $this->trackBusinessLayer->findAll($this->userId);
320
		}
321
		foreach ($tracks as &$track) {
322
			$artistId = $track->getArtistId();
323
			$albumId = $track->getAlbumId();
324
			$track = $track->toAPI($this->urlGenerator);
325
			if ($fulltree) {
326
				/** @var Artist $artist */
327
				$artist = $this->artistBusinessLayer->find($artistId, $this->userId);
328
				$track['artist'] = $artist->toAPI($this->urlGenerator, $this->l10n);
329
				$album = $this->albumBusinessLayer->find($albumId, $this->userId);
330
				$track['album'] = $album->toAPI($this->urlGenerator, $this->l10n);
331
			}
332
		}
333
		return new JSONResponse($tracks);
334
	}
335
336
	/**
337
	 * @NoAdminRequired
338
	 * @NoCSRFRequired
339
	 */
340
	public function track($trackIdOrSlug) {
341
		$trackId = $this->getIdFromSlug($trackIdOrSlug);
342
		/** @var Track $track */
343
		$track = $this->trackBusinessLayer->find($trackId, $this->userId);
344
		return new JSONResponse($track->toAPI($this->urlGenerator));
345
	}
346
347
	/**
348
	 * @NoAdminRequired
349
	 * @NoCSRFRequired
350
	 */
351
	public function trackByFileId(int $fileId) {
352
		$track = $this->trackBusinessLayer->findByFileId($fileId, $this->userId);
353
		if ($track !== null) {
354
			return new JSONResponse($track->toCollection($this->l10n));
355
		} else {
356
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
357
		}
358
	}
359
360
	/**
361
	 * @NoAdminRequired
362
	 * @NoCSRFRequired
363
	 */
364
	public function getScanState() {
365
		return new JSONResponse([
366
			'unscannedFiles' => $this->scanner->getUnscannedMusicFileIds($this->userId),
367
			'scannedCount' => $this->trackBusinessLayer->count($this->userId)
368
		]);
369
	}
370
371
	/**
372
	 * @NoAdminRequired
373
	 * @UseSession to keep the session reserved while execution in progress
374
	 */
375
	public function scan(string $files, $finalize) {
376
		// extract the parameters
377
		$fileIds = \array_map('intval', \explode(',', $files));
378
		$finalize = \filter_var($finalize, FILTER_VALIDATE_BOOLEAN);
379
380
		$filesScanned = $this->scanner->scanFiles($this->userId, $this->userFolder, $fileIds);
381
382
		$coversUpdated = false;
383
		if ($finalize) {
384
			$coversUpdated = $this->scanner->findAlbumCovers($this->userId)
385
							|| $this->scanner->findArtistCovers($this->userId);
386
			$totalCount = $this->trackBusinessLayer->count($this->userId);
387
			$this->logger->log("Scanning finished, user $this->userId has $totalCount scanned tracks in total", 'info');
388
		}
389
390
		return new JSONResponse([
391
			'filesScanned' => $filesScanned,
392
			'coversUpdated' => $coversUpdated
393
		]);
394
	}
395
396
	/**
397
	 * @NoAdminRequired
398
	 * @UseSession to keep the session reserved while execution in progress
399
	 */
400
	public function resetScanned() {
401
		$this->maintenance->resetDb($this->userId);
402
		return new JSONResponse(['success' => true]);
403
	}
404
405
	/**
406
	 * @NoAdminRequired
407
	 * @NoCSRFRequired
408
	 */
409
	public function download(int $fileId) {
410
		$nodes = $this->userFolder->getById($fileId);
411
		$node = $nodes[0] ?? null;
412
		if ($node instanceof \OCP\Files\File) {
413
			return new FileStreamResponse($node);
414
		}
415
416
		return new ErrorResponse(Http::STATUS_NOT_FOUND, 'file not found');
417
	}
418
419
	/**
420
	 * @NoAdminRequired
421
	 * @NoCSRFRequired
422
	 */
423
	public function filePath(int $fileId) {
424
		$nodes = $this->userFolder->getById($fileId);
425
		if (\count($nodes) == 0) {
426
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
427
		} else {
428
			$node = $nodes[0];
429
			$path = $this->userFolder->getRelativePath($node->getPath());
430
			return new JSONResponse(['path' => Util::urlEncodePath($path)]);
431
		}
432
	}
433
434
	/**
435
	 * @NoAdminRequired
436
	 * @NoCSRFRequired
437
	 */
438
	public function fileInfo(int $fileId) {
439
		$info = $this->scanner->getFileInfo($fileId, $this->userId, $this->userFolder);
440
		if ($info) {
441
			return new JSONResponse($info);
442
		} else {
443
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
444
		}
445
	}
446
447
	/**
448
	 * @NoAdminRequired
449
	 * @NoCSRFRequired
450
	 */
451
	public function fileDetails(int $fileId) {
452
		$details = $this->detailsHelper->getDetails($fileId, $this->userFolder);
453
		if ($details) {
454
			// metadata extracted, attempt to include also the data from Last.fm
455
			$track = $this->trackBusinessLayer->findByFileId($fileId, $this->userId);
456
			if ($track) {
457
				$details['lastfm'] = $this->lastfmService->getTrackInfo($track->getId(), $this->userId);
458
			} else {
459
				$this->logger->log("Track with file ID $fileId was not found => can't fetch info from Last.fm", 'warn');
460
			}
461
462
			return new JSONResponse($details);
463
		} else {
464
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
465
		}
466
	}
467
468
	/**
469
	 * @NoAdminRequired
470
	 * @NoCSRFRequired
471
	 */
472
	public function scrobble(int $trackId) {
473
		try {
474
			$this->trackBusinessLayer->recordTrackPlayed($trackId, $this->userId);
475
			return new JSONResponse(['success' => true]);
476
		} catch (BusinessLayerException $e) {
477
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
478
		}
479
	}
480
481
	/**
482
	 * @NoAdminRequired
483
	 * @NoCSRFRequired
484
	 */
485
	public function albumDetails($albumIdOrSlug) {
486
		try {
487
			$albumId = $this->getIdFromSlug($albumIdOrSlug);
488
			$info = $this->lastfmService->getAlbumInfo($albumId, $this->userId);
489
			return new JSONResponse($info);
490
		} catch (BusinessLayerException $e) {
491
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
492
		}
493
	}
494
495
	/**
496
	 * @NoAdminRequired
497
	 * @NoCSRFRequired
498
	 */
499
	public function artistDetails($artistIdOrSlug) {
500
		try {
501
			$artistId = $this->getIdFromSlug($artistIdOrSlug);
502
			$info = $this->lastfmService->getArtistInfo($artistId, $this->userId);
503
			return new JSONResponse($info);
504
		} catch (BusinessLayerException $e) {
505
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
506
		}
507
	}
508
509
	/**
510
	 * @NoAdminRequired
511
	 * @NoCSRFRequired
512
	 */
513
	public function similarArtists($artistIdOrSlug) {
514
		try {
515
			$artistId = $this->getIdFromSlug($artistIdOrSlug);
516
			$similar = $this->lastfmService->getSimilarArtists($artistId, $this->userId, /*includeNotPresent=*/true);
517
			return new JSONResponse(\array_map(function ($artist) {
518
				return [
519
					'id' => $artist->getId(),
520
					'name' => $artist->getName(),
521
					'url' => $artist->getLastfmUrl()
522
				];
523
			}, $similar));
524
		} catch (BusinessLayerException $e) {
525
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
526
		}
527
	}
528
529
	/**
530
	 * @PublicPage
531
	 * @NoCSRFRequired
532
	 */
533
	public function albumCover($albumIdOrSlug, $originalSize, $coverToken) {
534
		try {
535
			$userId = $this->userId ?? $this->coverHelper->getUserForAccessToken($coverToken);
536
			$albumId = $this->getIdFromSlug($albumIdOrSlug);
537
			$album = $this->albumBusinessLayer->find($albumId, $userId);
538
			return $this->cover($album, $userId, $originalSize);
539
		} catch (BusinessLayerException | \OutOfBoundsException $ex) {
540
			$this->logger->log("Failed to get the requested cover: $ex", 'debug');
541
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
542
		}
543
	}
544
545
	/**
546
	 * @PublicPage
547
	 * @NoCSRFRequired
548
	 */
549
	public function artistCover($artistIdOrSlug, $originalSize, $coverToken) {
550
		try {
551
			$userId = $this->userId ?? $this->coverHelper->getUserForAccessToken($coverToken);
552
			$artistId = $this->getIdFromSlug($artistIdOrSlug);
553
			$artist = $this->artistBusinessLayer->find($artistId, $userId);
554
			return $this->cover($artist, $userId, $originalSize);
555
		} catch (BusinessLayerException | \OutOfBoundsException $ex) {
556
			$this->logger->log("Failed to get the requested cover: $ex", 'debug');
557
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
558
		}
559
	}
560
561
	private function cover($entity, $userId, $originalSize) {
562
		$originalSize = \filter_var($originalSize, FILTER_VALIDATE_BOOLEAN);
563
		$userFolder = $this->userFolder ?? $this->scanner->resolveUserFolder($userId);
564
565
		if ($originalSize) {
566
			// cover requested in original size, without scaling or cropping
567
			$cover = $this->coverHelper->getCover($entity, $userId, $userFolder, CoverHelper::DO_NOT_CROP_OR_SCALE);
568
			if ($cover !== null) {
569
				return new FileResponse($cover);
570
			} else {
571
				return new ErrorResponse(Http::STATUS_NOT_FOUND);
572
			}
573
		} else {
574
			$coverAndHash = $this->coverHelper->getCoverAndHash($entity, $userId, $userFolder);
575
576
			if ($coverAndHash['hash'] !== null && $this->userId !== null) {
577
				// Cover is in cache. Return a redirection response so that the client
578
				// will fetch the content through a cacheable route.
579
				// The redirection is not used in case this is a call from the Firefox mediaSession API with not
580
				// logged in user.
581
				$link = $this->urlGenerator->linkToRoute('music.api.cachedCover', ['hash' => $coverAndHash['hash']]);
582
				return new RedirectResponse($link);
583
			} elseif ($coverAndHash['data'] !== null) {
584
				return new FileResponse($coverAndHash['data']);
585
			} else {
586
				return new ErrorResponse(Http::STATUS_NOT_FOUND);
587
			}
588
		}
589
	}
590
591
	/**
592
	 * @PublicPage
593
	 * @NoCSRFRequired
594
	 */
595
	public function cachedCover(string $hash, ?string $coverToken) {
596
		try {
597
			$userId = $this->userId ?? $this->coverHelper->getUserForAccessToken($coverToken);
598
			$coverData = $this->coverHelper->getCoverFromCache($hash, $userId);
599
			if ($coverData === null) {
600
				throw new \OutOfBoundsException("Cover with hash $hash not found");
601
			}
602
			$response =  new FileResponse($coverData);
603
			// instruct also the client-side to cache the result, this is safe
604
			// as the resource URI contains the image hash
605
			self::setClientCaching($response);
606
			return $response;
607
		} catch (\OutOfBoundsException $ex) {
608
			$this->logger->log("Failed to get the requested cover: $ex", 'debug');
609
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
610
		}
611
	}
612
613
	private static function setClientCaching(Response &$httpResponse, int $days=365) : void {
614
		$httpResponse->cacheFor($days * 24 * 60 * 60);
615
		$httpResponse->addHeader('Pragma', 'cache');
616
	}
617
}
618