Passed
Push — feature/909_Ampache_API_improv... ( 673c0e...814067 )
by Pauli
02:45
created

AlbumBusinessLayer   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 387
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 126
c 0
b 0
f 0
dl 0
loc 387
rs 4.5599
wmc 58

27 Methods

Rating   Name   Duplication   Size   Complexity  
A find() 0 3 1
A findAllByArtist() 0 3 1
B findAll() 0 6 7
A findAllByNameRecursive() 0 4 1
A __construct() 0 4 1
A findById() 0 6 2
A findAllByGenre() 0 3 1
A injectAlbumsToTracks() 0 22 5
A findAllByAlbumArtist() 0 11 3
A removeCovers() 0 2 1
B findAllByYearRange() 0 25 8
A findAllStarred() 0 3 1
A countByAlbumArtist() 0 2 1
A findAllAdvanced() 0 3 1
A findCovers() 0 9 3
A findAllByName() 0 5 1
A findAlbumOwner() 0 7 2
A findFrequentPlay() 0 4 1
A albumCoverIsOneOfFiles() 0 3 2
A findAlbumsWithCoversForTracks() 0 15 4
A findNotRecentPlay() 0 4 1
A findRecentPlay() 0 4 1
A setCover() 0 2 1
A addOrUpdateAlbum() 0 13 1
A updateFolderCover() 0 2 1
A countByArtist() 0 2 1
A injectExtraFields() 0 24 5

How to fix   Complexity   

Complex Class

Complex classes like AlbumBusinessLayer 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 AlbumBusinessLayer, and based on these observations, apply Extract Interface, too.

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 2016 - 2023
13
 */
14
15
namespace OCA\Music\BusinessLayer;
16
17
use OCA\Music\AppFramework\BusinessLayer\BusinessLayer;
18
use OCA\Music\AppFramework\BusinessLayer\BusinessLayerException;
19
use OCA\Music\AppFramework\Core\Logger;
20
21
use OCA\Music\Db\AlbumMapper;
22
use OCA\Music\Db\Album;
23
use OCA\Music\Db\Entity;
24
use OCA\Music\Db\MatchMode;
25
use OCA\Music\Db\SortBy;
26
use OCA\Music\Db\Track;
27
28
use OCA\Music\Utility\Util;
29
30
/**
31
 * Base class functions with the actually used inherited types to help IDE and Scrutinizer:
32
 * @phpstan-extends BusinessLayer<Album>
33
 */
34
class AlbumBusinessLayer extends BusinessLayer {
35
	protected $mapper; // eclipse the definition from the base class, to help IDE and Scrutinizer to know the actual type
36
	private $logger;
37
38
	public function __construct(AlbumMapper $albumMapper, Logger $logger) {
39
		parent::__construct($albumMapper);
40
		$this->mapper = $albumMapper;
41
		$this->logger = $logger;
42
	}
43
44
	/**
45
	 * {@inheritdoc}
46
	 * @see BusinessLayer::find()
47
	 * @return Album
48
	 */
49
	public function find(int $albumId, string $userId) : Entity {
50
		$album = parent::find($albumId, $userId);
51
		return $this->injectExtraFields([$album], $userId)[0];
52
	}
53
54
	/**
55
	 * {@inheritdoc}
56
	 * @see BusinessLayer::findById()
57
	 * @return Album[]
58
	 */
59
	public function findById(array $ids, string $userId=null, bool $preserveOrder=false) : array {
60
		$albums = parent::findById($ids, $userId, $preserveOrder);
61
		if ($userId !== null) {
62
			return $this->injectExtraFields($albums, $userId);
63
		} else {
64
			return $albums; // can't inject the extra fields without a user
65
		}
66
	}
67
68
	/**
69
	 * {@inheritdoc}
70
	 * @see BusinessLayer::findAll()
71
	 * @return Album[]
72
	 */
73
	public function findAll(string $userId, int $sortBy=SortBy::None, ?int $limit=null, ?int $offset=null,
74
							?string $createdMin=null, ?string $createdMax=null, ?string $updatedMin=null, ?string $updatedMax=null) : array {
75
		$albums = parent::findAll($userId, $sortBy, $limit, $offset, $createdMin, $createdMax, $updatedMin, $updatedMax);
76
		$effectivelyLimited = ($limit !== null && $limit < \count($albums));
77
		$everyAlbumIncluded = (!$effectivelyLimited && !$offset && !$createdMin && !$createdMax && !$updatedMin && !$updatedMax);
0 ignored issues
show
Bug Best Practice introduced by
The expression $offset of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
78
		return $this->injectExtraFields($albums, $userId, $everyAlbumIncluded);
79
	}
80
81
	/**
82
	 * Returns all albums filtered by name of album or artist
83
	 * @return Album[]
84
	 */
85
	public function findAllByNameRecursive(string $name, string $userId, ?int $limit=null, ?int $offset=null) : array {
86
		$name = \trim($name);
87
		$albums = $this->mapper->findAllByNameRecursive($name, $userId, $limit, $offset);
88
		return $this->injectExtraFields($albums, $userId);
89
	}
90
91
	/**
92
	 * Returns all albums filtered by artist (both album and track artists are considered)
93
	 * @return Album[] albums
94
	 */
95
	public function findAllByArtist(int $artistId, string $userId, ?int $limit=null, ?int $offset=null) : array {
96
		$albums = $this->mapper->findAllByArtist($artistId, $userId, $limit, $offset);
97
		return $this->injectExtraFields($albums, $userId);
98
	}
99
100
	/**
101
	 * Returns all albums filtered by album artist
102
	 * @param int|int[] $artistId
103
	 * @return Album[] albums
104
	 */
105
	public function findAllByAlbumArtist(/*mixed*/ $artistId, string $userId, ?int $limit=null, ?int $offset=null) : array {
106
		if (empty($artistId)) {
107
			return [];
108
		} else {
109
			if (!\is_array($artistId)) {
110
				$artistId = [$artistId];
111
			}
112
			$albums = $this->mapper->findAllByAlbumArtist($artistId, $userId, $limit, $offset);
113
			$albums = $this->injectExtraFields($albums, $userId);
114
			\usort($albums, ['\OCA\Music\Db\Album', 'compareYearAndName']);
115
			return $albums;
116
		}
117
	}
118
119
	/**
120
	 * Returns all albums filtered by genre
121
	 * @param int $genreId the genre to include
122
	 * @param string $userId the name of the user
123
	 * @param int|null $limit
124
	 * @param int|null $offset
125
	 * @return Album[] albums
126
	 */
127
	public function findAllByGenre(int $genreId, string $userId, ?int $limit=null, ?int $offset=null) : array {
128
		$albums = $this->mapper->findAllByGenre($genreId, $userId, $limit, $offset);
129
		return $this->injectExtraFields($albums, $userId);
130
	}
131
132
	/**
133
	 * Returns all albums filtered by release year
134
	 * @param int $fromYear
135
	 * @param int $toYear
136
	 * @param string $userId the name of the user
137
	 * @param int|null $limit
138
	 * @param int|null $offset
139
	 * @return Album[] albums
140
	 */
141
	public function findAllByYearRange(
142
			int $fromYear, int $toYear, string $userId, ?int $limit=null, ?int $offset=null) : array {
143
		$reverseOrder = false;
144
		if ($fromYear > $toYear) {
145
			$reverseOrder = true;
146
			Util::swap($fromYear, $toYear);
147
		}
148
149
		// Implement all the custom logic of this function here, without special Mapper function
150
		$albums = \array_filter($this->findAll($userId), function ($album) use ($fromYear, $toYear) {
151
			$years = $album->getYears();
152
			return (!empty($years) && \min($years) <= $toYear && \max($years) >= $fromYear);
153
		});
154
155
		\usort($albums, function ($album1, $album2) use ($reverseOrder) {
156
			return $reverseOrder
157
				? $album2->yearToAPI() - $album1->yearToAPI()
158
				: $album1->yearToAPI() - $album2->yearToAPI();
159
		});
160
161
		if ($limit !== null || $offset !== null) {
162
			$albums = \array_slice($albums, $offset ?: 0, $limit);
163
		}
164
165
		return $albums;
166
	}
167
168
	/**
169
	 * {@inheritdoc}
170
	 * @see BusinessLayer::findAllByName()
171
	 * @return Album[]
172
	 */
173
	public function findAllByName(
174
			?string $name, string $userId, int $matchMode=MatchMode::Exact, ?int $limit=null, ?int $offset=null,
175
			?string $createdMin=null, ?string $createdMax=null, ?string $updatedMin=null, ?string $updatedMax=null) : array {
176
		$albums = parent::findAllByName($name, $userId, $matchMode, $limit, $offset, $createdMin, $createdMax, $updatedMin, $updatedMax);
177
		return $this->injectExtraFields($albums, $userId);
178
	}
179
180
	/**
181
	 * {@inheritdoc}
182
	 * @see BusinessLayer::findAllByName()
183
	 * @return Album[]
184
	 */
185
	public function findAllStarred(string $userId, ?int $limit=null, ?int $offset=null) : array {
186
		$albums = parent::findAllStarred($userId, $limit, $offset);
187
		return $this->injectExtraFields($albums, $userId);
188
	}
189
190
	/**
191
	 * {@inheritdoc}
192
	 * @see BusinessLayer::findAllByName()
193
	 * @return Album[]
194
	 */
195
	public function findAllAdvanced(string $conjunction, array $rules, string $userId, ?int $limit=null, ?int $offset=null) : array {
196
		$albums = parent::findAllAdvanced($conjunction, $rules, $userId, $limit, $offset);
197
		return $this->injectExtraFields($albums, $userId);
198
	}
199
200
	/**
201
	 * Find most frequently played albums, judged by the total play count of the contained tracks
202
	 * @return Album[]
203
	 */
204
	public function findFrequentPlay(string $userId, ?int $limit=null, ?int $offset=null) : array {
205
		$countsPerAlbum = $this->mapper->getAlbumTracksPlayCount($userId, $limit, $offset);
206
		$ids = \array_keys($countsPerAlbum);
207
		return $this->findById($ids, $userId, /*preserveOrder=*/true);
208
	}
209
210
	/**
211
	 * Find most recently played albums
212
	 * @return Album[]
213
	 */
214
	public function findRecentPlay(string $userId, ?int $limit=null, ?int $offset=null) : array {
215
		$playTimePerAlbum = $this->mapper->getLatestAlbumPlayTimes($userId, $limit, $offset);
216
		$ids = \array_keys($playTimePerAlbum);
217
		return $this->findById($ids, $userId, /*preserveOrder=*/true);
218
	}
219
220
	/**
221
	 * Find least recently played albums
222
	 * @return Album[]
223
	 */
224
	public function findNotRecentPlay(string $userId, ?int $limit=null, ?int $offset=null) : array {
225
		$playTimePerAlbum = $this->mapper->getFurthestAlbumPlayTimes($userId, $limit, $offset);
226
		$ids = \array_keys($playTimePerAlbum);
227
		return $this->findById($ids, $userId, /*preserveOrder=*/true);
228
	}
229
230
	/**
231
	 * Add performing artists, release years, genres, and disk counts to the given album objects
232
	 * @param Album[] $albums
233
	 * @param string $userId
234
	 * @param bool $allAlbums Set to true if $albums contains all albums of the user.
235
	 *                        This has now effect on the outcome but helps in optimizing
236
	 *                        the database query.
237
	 * @return Album[]
238
	 */
239
	private function injectExtraFields(array $albums, string $userId, bool $allAlbums = false) : array {
240
		if (\count($albums) > 0) {
241
			// In case we are injecting data to a lot of albums, do not limit the
242
			// SQL SELECTs to only those albums. Very large amount of SQL host parameters
243
			// could cause problems with SQLite (see #239) and probably it would be bad for
244
			// performance also on other DBMSs. For the proper operation of this function,
245
			// it doesn't matter if we fetch data for some extra albums.
246
			$albumIds = ($allAlbums || \count($albums) >= self::MAX_SQL_ARGS)
247
					? null : Util::extractIds($albums);
248
249
			$artists = $this->mapper->getPerformingArtistsByAlbumId($albumIds, $userId);
250
			$years = $this->mapper->getYearsByAlbumId($albumIds, $userId);
251
			$diskCounts = $this->mapper->getDiscCountByAlbumId($albumIds, $userId);
252
			$genres = $this->mapper->getGenresByAlbumId($albumIds, $userId);
253
254
			foreach ($albums as &$album) {
255
				$albumId = $album->getId();
256
				$album->setArtistIds($artists[$albumId] ?? []);
257
				$album->setNumberOfDisks($diskCounts[$albumId] ?? 1);
258
				$album->setGenres($genres[$albumId] ?? null);
259
				$album->setYears($years[$albumId] ?? null);
260
			}
261
		}
262
		return $albums;
263
	}
264
265
	/**
266
	 * Returns the count of albums where the given Artist is featured in
267
	 * @param integer $artistId
268
	 * @return integer
269
	 */
270
	public function countByArtist(int $artistId) : int {
271
		return $this->mapper->countByArtist($artistId);
272
	}
273
274
	/**
275
	 * Returns the count of albums where the given artist is the album artist
276
	 * @param integer $artistId
277
	 * @return integer
278
	 */
279
	public function countByAlbumArtist(int $artistId) : int {
280
		return $this->mapper->countByAlbumArtist($artistId);
281
	}
282
283
	public function findAlbumOwner(int $albumId) : string {
284
		$entities = $this->findById([$albumId]);
285
		if (\count($entities) != 1) {
286
			throw new BusinessLayerException(
287
					'Expected to find one album but got ' . \count($entities));
288
		} else {
289
			return $entities[0]->getUserId();
290
		}
291
	}
292
293
	/**
294
	 * Adds an album if it does not exist already or updates an existing album
295
	 * @param string|null $name the name of the album
296
	 * @param integer $albumArtistId
297
	 * @param string $userId
298
	 * @return Album The added/updated album
299
	 */
300
	public function addOrUpdateAlbum(?string $name, int $albumArtistId, string $userId) : Album {
301
		$album = new Album();
302
		$album->setName(Util::truncate($name, 256)); // some DB setups can't truncate automatically to column max size
303
		$album->setUserId($userId);
304
		$album->setAlbumArtistId($albumArtistId);
305
306
		// Generate hash from the set of fields forming the album identity to prevent duplicates.
307
		// The uniqueness of album name is evaluated in case-insensitive manner.
308
		$lowerName = \mb_strtolower($album->getName() ?? '');
309
		$hash = \hash('md5', "$lowerName|$albumArtistId");
310
		$album->setHash($hash);
311
312
		return $this->mapper->updateOrInsert($album);
313
	}
314
315
	/**
316
	 * Check if given file is used as cover for the given album
317
	 * @param int $albumId
318
	 * @param int[] $fileIds
319
	 * @return boolean
320
	 */
321
	public function albumCoverIsOneOfFiles(int $albumId, array $fileIds) : bool {
322
		$albums = $this->findById([$albumId]);
323
		return (\count($albums) && \in_array($albums[0]->getCoverFileId(), $fileIds));
324
	}
325
326
	/**
327
	 * updates the cover for albums in the specified folder without cover
328
	 * @param integer $coverFileId the file id of the cover image
329
	 * @param integer $folderId the file id of the folder where the albums are looked from
330
	 * @return boolean True if one or more albums were influenced
331
	 */
332
	public function updateFolderCover(int $coverFileId, int $folderId) : bool {
333
		return $this->mapper->updateFolderCover($coverFileId, $folderId);
334
	}
335
336
	/**
337
	 * set cover file for a specified album
338
	 * @param int|null $coverFileId the file id of the cover image
339
	 * @param int $albumId the id of the album to be modified
340
	 */
341
	public function setCover(?int $coverFileId, int $albumId) {
342
		$this->mapper->setCover($coverFileId, $albumId);
343
	}
344
345
	/**
346
	 * removes the cover art from albums, replacement covers will be searched in a background task
347
	 * @param integer[] $coverFileIds the file IDs of the cover images
348
	 * @param string[]|null $userIds the users whose music library is targeted; all users are targeted if omitted
349
	 * @return Album[] albums which got modified, empty array if none
350
	 */
351
	public function removeCovers(array $coverFileIds, array $userIds=null) : array {
352
		return $this->mapper->removeCovers($coverFileIds, $userIds);
353
	}
354
355
	/**
356
	 * try to find cover arts for albums without covers
357
	 * @param string|null $userId target user; omit to target all users
358
	 * @return string[] users whose collections got modified
359
	 */
360
	public function findCovers(string $userId = null) : array {
361
		$affectedUsers = [];
362
		$albums = $this->mapper->getAlbumsWithoutCover($userId);
363
		foreach ($albums as $album) {
364
			if ($this->mapper->findAlbumCover($album['albumId'], $album['parentFolderId'])) {
365
				$affectedUsers[$album['userId']] = 1;
366
			}
367
		}
368
		return \array_keys($affectedUsers);
369
	}
370
371
	/**
372
	 * Given an array of track IDs, find corresponding unique album IDs, including only
373
	 * those album which have a cover art set.
374
	 * @param int[] $trackIds
375
	 * @return Album[] *Partial* albums, without any injected extra fields
376
	 */
377
	public function findAlbumsWithCoversForTracks(array $trackIds, string $userId, int $limit) : array {
378
		if (\count($trackIds) === 0) {
379
			return [];
380
		} else {
381
			$result = [];
382
			$idChunks = \array_chunk($trackIds, self::MAX_SQL_ARGS - 1);
383
			foreach ($idChunks as $idChunk) {
384
				$resultChunk = $this->mapper->findAlbumsWithCoversForTracks($idChunk, $userId, $limit);
385
				$result = \array_merge($result, $resultChunk);
386
				$limit -= \count($resultChunk);
387
				if ($limit <= 0) {
388
					break;
389
				}
390
			}
391
			return $result;
392
		}
393
	}
394
395
	/**
396
	 * Given an array of Track objects, inject the corresponding Album object to each of them
397
	 * @param Track[] $tracks (in|out)
398
	 */
399
	public function injectAlbumsToTracks(array &$tracks, string $userId) {
400
		$albumIds = [];
401
402
		// get unique album IDs
403
		foreach ($tracks as $track) {
404
			$albumIds[$track->getAlbumId()] = 1;
405
		}
406
		$albumIds = \array_keys($albumIds);
407
408
		// get the corresponding entities from the business layer
409
		if (\count($albumIds) < self::MAX_SQL_ARGS && \count($albumIds) < $this->count($userId)) {
410
			$albums = $this->findById($albumIds, $userId);
411
		} else {
412
			$albums = $this->findAll($userId);
413
		}
414
415
		// create hash tables "id => entity" for the albums for fast access
416
		$albumMap = Util::createIdLookupTable($albums);
417
418
		// finally, set the references on the tracks
419
		foreach ($tracks as &$track) {
420
			$track->setAlbum($albumMap[$track->getAlbumId()] ?? new Album());
421
		}
422
	}
423
}
424