Passed
Push — master ( ce9a66...c3a2b5 )
by Pauli
02:52
created

CoverApiController::cachedCover()   A

Complexity

Conditions 3
Paths 6

Size

Total Lines 15
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 11
nc 6
nop 2
dl 0
loc 15
rs 9.9
c 1
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 Pauli Järvinen <[email protected]>
10
 * @copyright Pauli Järvinen 2024
11
 */
12
13
namespace OCA\Music\Controller;
14
15
use OCP\AppFramework\Controller;
16
use OCP\AppFramework\Http;
17
use OCP\AppFramework\Http\RedirectResponse;
18
use OCP\AppFramework\Http\Response;
19
use OCP\Files\IRootFolder;
20
use OCP\IRequest;
21
use OCP\IURLGenerator;
22
23
use OCA\Music\AppFramework\BusinessLayer\BusinessLayerException;
24
use OCA\Music\AppFramework\Core\Logger;
25
use OCA\Music\BusinessLayer\AlbumBusinessLayer;
26
use OCA\Music\BusinessLayer\ArtistBusinessLayer;
27
use OCA\Music\Db\Album;
28
use OCA\Music\Db\Artist;
29
use OCA\Music\Http\ErrorResponse;
30
use OCA\Music\Http\FileResponse;
31
use OCA\Music\Utility\CoverHelper;
32
33
class CoverApiController extends Controller {
34
35
	private IURLGenerator $urlGenerator;
36
	private IRootFolder $rootFolder;
37
	private ArtistBusinessLayer $artistBusinessLayer;
38
	private AlbumBusinessLayer $albumBusinessLayer;
39
	private CoverHelper $coverHelper;
40
	private ?string $userId;
41
	private Logger $logger;
42
43
	public function __construct(string $appname,
44
								IRequest $request,
45
								IURLGenerator $urlGenerator,
46
								IRootFolder $rootFolder,
47
								ArtistBusinessLayer $artistBusinessLayer,
48
								AlbumBusinessLayer $albumBusinessLayer,
49
								CoverHelper $coverHelper,
50
								?string $userId, // null if this gets called after the user has logged out or on a public page
51
								Logger $logger) {
52
		parent::__construct($appname, $request);
53
		$this->urlGenerator = $urlGenerator;
54
		$this->rootFolder = $rootFolder;
55
		$this->artistBusinessLayer = $artistBusinessLayer;
56
		$this->albumBusinessLayer = $albumBusinessLayer;
57
		$this->coverHelper = $coverHelper;
58
		$this->userId = $userId;
59
		$this->logger = $logger;
60
	}
61
62
	/**
63
	 * @PublicPage
64
	 * @NoCSRFRequired
65
	 */
66
	public function albumCover(int $albumId, $originalSize, $coverToken) {
67
		try {
68
			$userId = $this->userId ?? $this->coverHelper->getUserForAccessToken($coverToken);
69
			$album = $this->albumBusinessLayer->find($albumId, $userId);
70
			return $this->cover($album, $userId, $originalSize);
71
		} catch (BusinessLayerException | \OutOfBoundsException $ex) {
72
			$this->logger->log("Failed to get the requested cover: $ex", 'debug');
73
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
74
		}
75
	}
76
77
	/**
78
	 * @PublicPage
79
	 * @NoCSRFRequired
80
	 */
81
	public function artistCover(int $artistId, $originalSize, $coverToken) {
82
		try {
83
			$userId = $this->userId ?? $this->coverHelper->getUserForAccessToken($coverToken);
84
			$artist = $this->artistBusinessLayer->find($artistId, $userId);
85
			return $this->cover($artist, $userId, $originalSize);
86
		} catch (BusinessLayerException | \OutOfBoundsException $ex) {
87
			$this->logger->log("Failed to get the requested cover: $ex", 'debug');
88
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
89
		}
90
	}
91
92
	/**
93
	 * @PublicPage
94
	 * @NoCSRFRequired
95
	 */
96
	public function cachedCover(string $hash, ?string $coverToken) {
97
		try {
98
			$userId = $this->userId ?? $this->coverHelper->getUserForAccessToken($coverToken);
99
			$coverData = $this->coverHelper->getCoverFromCache($hash, $userId);
100
			if ($coverData === null) {
101
				throw new \OutOfBoundsException("Cover with hash $hash not found");
102
			}
103
			$response =  new FileResponse($coverData);
104
			// instruct also the client-side to cache the result, this is safe
105
			// as the resource URI contains the image hash
106
			self::setClientCaching($response);
107
			return $response;
108
		} catch (\OutOfBoundsException $ex) {
109
			$this->logger->log("Failed to get the requested cover: $ex", 'debug');
110
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
111
		}
112
	}
113
114
	/**
115
	 * @param Artist|Album $entity
116
	 */
117
	private function cover($entity, string $userId, $originalSize) {
118
		$originalSize = \filter_var($originalSize, FILTER_VALIDATE_BOOLEAN);
119
		$userFolder = $this->rootFolder->getUserFolder($userId);
120
121
		if ($originalSize) {
122
			// cover requested in original size, without scaling or cropping
123
			$cover = $this->coverHelper->getCover($entity, $userId, $userFolder, CoverHelper::DO_NOT_CROP_OR_SCALE);
124
			if ($cover !== null) {
125
				return new FileResponse($cover);
126
			} else {
127
				return new ErrorResponse(Http::STATUS_NOT_FOUND);
128
			}
129
		} else {
130
			$coverAndHash = $this->coverHelper->getCoverAndHash($entity, $userId, $userFolder);
131
132
			if ($coverAndHash['hash'] !== null && $this->userId !== null) {
133
				// Cover is in cache. Return a redirection response so that the client
134
				// will fetch the content through a cacheable route.
135
				// The redirection is not used in case this is a call from the Firefox mediaSession API with not
136
				// logged in user.
137
				$link = $this->urlGenerator->linkToRoute('music.coverApi.cachedCover', ['hash' => $coverAndHash['hash']]);
138
				return new RedirectResponse($link);
139
			} elseif ($coverAndHash['data'] !== null) {
140
				return new FileResponse($coverAndHash['data']);
141
			} else {
142
				return new ErrorResponse(Http::STATUS_NOT_FOUND);
143
			}
144
		}
145
	}
146
147
	private static function setClientCaching(Response &$httpResponse, int $days=365) : void {
148
		$httpResponse->cacheFor($days * 24 * 60 * 60);
149
		$httpResponse->addHeader('Pragma', 'cache');
150
	}
151
}
152