Total Complexity | 50 |
Total Lines | 343 |
Duplicated Lines | 0 % |
Changes | 3 | ||
Bugs | 0 | Features | 1 |
Complex classes like CoverHelper 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 CoverHelper, and based on these observations, apply Extract Interface, too.
1 | <?php declare(strict_types=1); |
||
30 | class CoverHelper { |
||
31 | private $extractor; |
||
32 | private $cache; |
||
33 | private $coverSize; |
||
34 | private $logger; |
||
35 | |||
36 | const MAX_SIZE_TO_CACHE = 102400; |
||
37 | const DO_NOT_CROP_OR_SCALE = -1; |
||
38 | |||
39 | public function __construct( |
||
40 | Extractor $extractor, |
||
41 | Cache $cache, |
||
42 | IConfig $config, |
||
43 | Logger $logger) { |
||
44 | $this->extractor = $extractor; |
||
45 | $this->cache = $cache; |
||
46 | $this->logger = $logger; |
||
47 | |||
48 | // Read the cover size to use from config.php or use the default |
||
49 | $this->coverSize = \intval($config->getSystemValue('music.cover_size')) ?: 380; |
||
50 | } |
||
51 | |||
52 | /** |
||
53 | * Get cover image of an album or and artist |
||
54 | * |
||
55 | * @param Album|Artist|PodcastChannel $entity |
||
56 | * @param string $userId |
||
57 | * @param Folder $rootFolder |
||
58 | * @param int|null $size Desired (max) image size, null to use the default. |
||
59 | * Special value DO_NOT_CROP_OR_SCALE can be used to opt out of |
||
60 | * scaling and cropping altogether. |
||
61 | * @return array|null Image data in format accepted by \OCA\Music\Http\FileResponse |
||
62 | */ |
||
63 | public function getCover($entity, string $userId, Folder $rootFolder, int $size=null) { |
||
64 | // Skip using cache in case the cover is requested in specific size |
||
65 | if ($size !== null) { |
||
66 | return $this->readCover($entity, $rootFolder, $size); |
||
67 | } else { |
||
68 | $dataAndHash = $this->getCoverAndHash($entity, $userId, $rootFolder); |
||
|
|||
69 | return $dataAndHash['data']; |
||
70 | } |
||
71 | } |
||
72 | |||
73 | /** |
||
74 | * Get cover image of an album or and artist along with the image's hash |
||
75 | * |
||
76 | * The hash is non-null only in case the cover is/was cached. |
||
77 | * |
||
78 | * @param Album|Artist $entity |
||
79 | * @param string $userId |
||
80 | * @param Folder $rootFolder |
||
81 | * @return array Dictionary with keys 'data' and 'hash' |
||
82 | */ |
||
83 | public function getCoverAndHash($entity, string $userId, Folder $rootFolder) : array { |
||
84 | $hash = $this->cache->get($userId, self::getHashKey($entity)); |
||
85 | $data = null; |
||
86 | |||
87 | if ($hash !== null) { |
||
88 | $data = $this->getCoverFromCache($hash, $userId); |
||
89 | } |
||
90 | if ($data === null) { |
||
91 | $hash = null; |
||
92 | $data = $this->readCover($entity, $rootFolder, $this->coverSize); |
||
93 | if ($data !== null) { |
||
94 | $hash = $this->addCoverToCache($entity, $userId, $data); |
||
95 | } |
||
96 | } |
||
97 | |||
98 | return ['data' => $data, 'hash' => $hash]; |
||
99 | } |
||
100 | |||
101 | /** |
||
102 | * Get all album cover hashes for one user. |
||
103 | * @param string $userId |
||
104 | * @return array with album IDs as keys and hashes as values |
||
105 | */ |
||
106 | public function getAllCachedAlbumCoverHashes(string $userId) : array { |
||
107 | $rows = $this->cache->getAll($userId, 'album_cover_hash_'); |
||
108 | $hashes = []; |
||
109 | $prefixLen = \strlen('album_cover_hash_'); |
||
110 | foreach ($rows as $row) { |
||
111 | $albumId = \substr($row['key'], $prefixLen); |
||
112 | $hashes[$albumId] = $row['data']; |
||
113 | } |
||
114 | return $hashes; |
||
115 | } |
||
116 | |||
117 | /** |
||
118 | * Get cover image with given hash from the cache |
||
119 | * |
||
120 | * @param string $hash |
||
121 | * @param string $userId |
||
122 | * @param bool $asBase64 |
||
123 | * @return array|null Image data in format accepted by \OCA\Music\Http\FileResponse |
||
124 | */ |
||
125 | public function getCoverFromCache(string $hash, string $userId, bool $asBase64 = false) { |
||
137 | } |
||
138 | |||
139 | /** |
||
140 | * Cache the given cover image data |
||
141 | * @param Album|Artist $entity |
||
142 | * @param string $userId |
||
143 | * @param array $coverData |
||
144 | * @return string|null Hash of the cached cover |
||
145 | */ |
||
146 | private function addCoverToCache($entity, string $userId, array $coverData) { |
||
176 | } |
||
177 | |||
178 | /** |
||
179 | * Remove album cover image from cache if it is there. Silently do nothing if there |
||
180 | * is no cached cover. All users are targeted if no $userId passed. |
||
181 | */ |
||
182 | public function removeAlbumCoverFromCache(int $albumId, string $userId=null) { |
||
184 | } |
||
185 | |||
186 | /** |
||
187 | * Remove artist cover image from cache if it is there. Silently do nothing if there |
||
188 | * is no cached cover. All users are targeted if no $userId passed. |
||
189 | */ |
||
190 | public function removeArtistCoverFromCache(int $artistId, string $userId=null) { |
||
192 | } |
||
193 | |||
194 | /** |
||
195 | * Read cover image from the entity-specific file or URL and scale it unless the caller opts out of it |
||
196 | * @param Album|Artist|PodcastChannel $entity |
||
197 | * @param Folder $rootFolder |
||
198 | * @param int $size Maximum size for the image to read, larger images are scaled down. |
||
199 | * Special value DO_NOT_CROP_OR_SCALE can be used to opt out of |
||
200 | * scaling and cropping altogether. |
||
201 | * @return array|null Image data in format accepted by \OCA\Music\Http\FileResponse |
||
202 | */ |
||
203 | private function readCover($entity, Folder $rootFolder, int $size) : ?array { |
||
219 | } |
||
220 | |||
221 | /** |
||
222 | * Read cover image from the file system |
||
223 | * @param Album|Artist $entity |
||
224 | * @param Folder $rootFolder |
||
225 | * @return array|null Image data in format accepted by \OCA\Music\Http\FileResponse |
||
226 | */ |
||
227 | private function readCoverFromLocalFile($entity, Folder $rootFolder) { |
||
228 | $response = null; |
||
229 | |||
230 | $coverId = $entity->getCoverFileId(); |
||
231 | if ($coverId > 0) { |
||
232 | $node = $rootFolder->getById($coverId)[0] ?? null; |
||
233 | if ($node instanceof File) { |
||
234 | $mime = $node->getMimeType(); |
||
235 | |||
236 | if (\strpos($mime, 'audio') === 0) { // embedded cover image |
||
237 | $cover = $this->extractor->parseEmbeddedCoverArt($node); // TODO: currently only album cover supported |
||
238 | |||
239 | if ($cover !== null) { |
||
240 | $response = ['mimetype' => $cover['image_mime'], 'content' => $cover['data']]; |
||
241 | } |
||
242 | } else { // separate image file |
||
243 | $response = ['mimetype' => $mime, 'content' => $node->getContent()]; |
||
244 | } |
||
245 | } |
||
246 | |||
247 | if ($response === null) { |
||
248 | $class = \get_class($entity); |
||
249 | $this->logger->log("Requested cover not found for $class entity {$entity->getId()}, coverId=$coverId", 'error'); |
||
250 | } |
||
251 | } |
||
252 | |||
253 | return $response; |
||
254 | } |
||
255 | |||
256 | /** |
||
257 | * Scale down images to reduce size and crop to square shape |
||
258 | * |
||
259 | * If one of the dimensions of the image is smaller than the maximum, then just |
||
260 | * crop to square shape but do not scale. |
||
261 | * @param array $image The image to be scaled down in format accepted by \OCA\Music\Http\FileResponse |
||
262 | * @param integer $maxSize The maximum size in pixels for the square shaped output |
||
263 | * @return array The processed image in format accepted by \OCA\Music\Http\FileResponse |
||
264 | */ |
||
265 | public function scaleDownAndCrop(array $image, int $maxSize) : array { |
||
317 | } |
||
318 | |||
319 | private static function autoDetectMime($imageContent) { |
||
320 | return \getimagesizefromstring($imageContent)['mime']; |
||
321 | } |
||
322 | |||
323 | /** |
||
324 | * @param Album|Artist|PodcastChannel $entity |
||
325 | * @throws \InvalidArgumentException if entity is not one of the expected types |
||
326 | * @return string |
||
327 | */ |
||
328 | private static function getHashKey($entity) { |
||
337 | } |
||
338 | } |
||
339 | |||
340 | /** |
||
341 | * Create and store an access token which can be used to read cover images of a user. |
||
342 | * A user may have only one valid cover image access token at a time; the latest token |
||
343 | * always overwrites the previously obtained one. |
||
344 | * |
||
345 | * The reason this is needed is because the mediaSession in Firefox loads the cover images |
||
346 | * in a context where normal cookies and other standard request headers are not available. |
||
347 | * Hence, we need to provide the cover images as "public" resources, i.e. without requiring |
||
348 | * that the caller is logged in to the cloud. But still, we don't want to let just anyone |
||
349 | * load the user data. The solution is to use a temporary token which grants access just to |
||
350 | * the cover images. This token can be then sent as URL argument by the mediaSession. |
||
351 | */ |
||
352 | public function createAccessToken(string $userId) : string { |
||
358 | } |
||
359 | |||
360 | /** |
||
361 | * @see CoverHelper::createAccessToken |
||
362 | * @throws \OutOfBoundsException if the token is not valid |
||
363 | */ |
||
364 | public function getUserForAccessToken(?string $token) : string { |
||
375 |