Passed
Push — master ( b806b4...5b46ab )
by Pauli
02:42
created

AlbumBusinessLayer::findAll()   B

Complexity

Conditions 7
Paths 12

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 4
c 1
b 0
f 0
nc 12
nop 8
dl 0
loc 6
rs 8.8333

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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 - 2021
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\SortBy;
24
use \OCA\Music\Db\Track;
25
26
use \OCA\Music\Utility\Util;
27
28
use \OCP\AppFramework\Db\Entity;
29
30
/**
31
 * Base class functions with the actually used inherited types to help IDE and Scrutinizer:
32
 * @method Album find(int $trackId, string $userId)
33
 * @method Album[] findAll(string $userId, int $sortBy=SortBy::None, int $limit=null, int $offset=null)
34
 * @method Album[] findAllByName(string $name, string $userId, bool $fuzzy=false, int $limit=null, int $offset=null)
35
 * @method Album[] findById(int[] $ids, string $userId=null)
36
 */
37
class AlbumBusinessLayer extends BusinessLayer {
38
	protected $mapper; // eclipse the definition from the base class, to help IDE and Scrutinizer to know the actual type
39
	private $logger;
40
41
	private const MAX_SQL_ALBUM_ARGS = 998; // Some SQLite installations can't handle more than 999 query args. 1 slot reserved for `user_id`.
42
43
	public function __construct(AlbumMapper $albumMapper, Logger $logger) {
44
		parent::__construct($albumMapper);
45
		$this->mapper = $albumMapper;
46
		$this->logger = $logger;
47
	}
48
49
	/**
50
	 * {@inheritdoc}
51
	 * @see BusinessLayer::find()
52
	 * @return Album
53
	 */
54
	public function find(int $albumId, string $userId) : Entity {
55
		$album = parent::find($albumId, $userId);
56
		return $this->injectExtraFields([$album], $userId)[0];
57
	}
58
59
	/**
60
	 * {@inheritdoc}
61
	 * @see BusinessLayer::findAll()
62
	 * @return Album[]
63
	 */
64
	public function findAll(string $userId, int $sortBy=SortBy::None, int $limit=null, int $offset=null,
65
							?string $createdMin=null, ?string $createdMax=null, ?string $updatedMin=null, ?string $updatedMax=null) : array {
66
		$albums = parent::findAll($userId, $sortBy, $limit, $offset, $createdMin, $createdMax, $updatedMin, $updatedMax);
67
		$effectivelyLimited = ($limit !== null && $limit < \count($albums));
68
		$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...
69
		return $this->injectExtraFields($albums, $userId, $everyAlbumIncluded);
70
	}
71
72
	/**
73
	 * Returns all albums filtered by artist (both album and track artists are considered)
74
	 * @param integer $artistId the id of the artist
75
	 * @param string $userId the name of the user
76
	 * @return Album[] albums
77
	 */
78
	public function findAllByArtist(int $artistId, string $userId) : array {
79
		$albums = $this->mapper->findAllByArtist($artistId, $userId);
80
		return $this->injectExtraFields($albums, $userId);
81
	}
82
83
	/**
84
	 * Returns all albums filtered by album artist
85
	 * @param integer $artistId the id of the artist
86
	 * @param string $userId the name of the user
87
	 * @return Album[] albums
88
	 */
89
	public function findAllByAlbumArtist(int $artistId, string $userId) : array {
90
		$albums = $this->mapper->findAllByAlbumArtist($artistId, $userId);
91
		$albums = $this->injectExtraFields($albums, $userId);
92
		\usort($albums, ['\OCA\Music\Db\Album', 'compareYearAndName']);
93
		return $albums;
94
	}
95
96
	/**
97
	 * Returns all albums filtered by genre
98
	 * @param int $genreId the genre to include
99
	 * @param string $userId the name of the user
100
	 * @param int|null $limit
101
	 * @param int|null $offset
102
	 * @return Album[] albums
103
	 */
104
	public function findAllByGenre(int $genreId, string $userId, int $limit=null, int $offset=null) : array {
105
		$albums = $this->mapper->findAllByGenre($genreId, $userId, $limit, $offset);
106
		return $this->injectExtraFields($albums, $userId);
107
	}
108
109
	/**
110
	 * Returns all albums filtered by release year
111
	 * @param int $fromYear
112
	 * @param int $toYear
113
	 * @param string $userId the name of the user
114
	 * @param int|null $limit
115
	 * @param int|null $offset
116
	 * @return Album[] albums
117
	 */
118
	public function findAllByYearRange(
119
			int $fromYear, int $toYear, string $userId, int $limit=null, int $offset=null) : array {
120
		$reverseOrder = false;
121
		if ($fromYear > $toYear) {
122
			$reverseOrder = true;
123
			Util::swap($fromYear, $toYear);
124
		}
125
126
		// Implement all the custom logic of this function here, without special Mapper function
127
		$albums = \array_filter($this->findAll($userId), function ($album) use ($fromYear, $toYear) {
128
			$years = $album->getYears();
129
			return (!empty($years) && \min($years) <= $toYear && \max($years) >= $fromYear);
130
		});
131
132
		\usort($albums, function ($album1, $album2) use ($reverseOrder) {
133
			return $reverseOrder
134
				? $album2->yearToAPI() - $album1->yearToAPI()
135
				: $album1->yearToAPI() - $album2->yearToAPI();
136
		});
137
138
		if ($limit !== null || $offset !== null) {
139
			$albums = \array_slice($albums, $offset ?: 0, $limit);
140
		}
141
142
		return $albums;
143
	}
144
145
	/**
146
	 * {@inheritdoc}
147
	 * @see BusinessLayer::findAllByName()
148
	 * @return Album[]
149
	 */
150
	public function findAllByName(
151
			string $name, string $userId, bool $fuzzy = false, int $limit=null, int $offset=null,
152
			?string $createdMin=null, ?string $createdMax=null, ?string $updatedMin=null, ?string $updatedMax=null) : array {
153
		$albums = parent::findAllByName($name, $userId, $fuzzy, $limit, $offset, $createdMin, $createdMax, $updatedMin, $updatedMax);
154
		return $this->injectExtraFields($albums, $userId);
155
	}
156
157
	/**
158
	 * Add performing artists, release years, genres, and disk counts to the given album objects
159
	 * @param Album[] $albums
160
	 * @param string $userId
161
	 * @param bool $allAlbums Set to true if $albums contains all albums of the user.
162
	 *                        This has now effect on the outcome but helps in optimizing
163
	 *                        the database query.
164
	 * @return Album[]
165
	 */
166
	private function injectExtraFields(array $albums, string $userId, bool $allAlbums = false) : array {
167
		if (\count($albums) > 0) {
168
			// In case we are injecting data to a lot of albums, do not limit the
169
			// SQL SELECTs to only those albums. Very large amount of SQL host parameters
170
			// could cause problems with SQLite (see #239) and probably it would be bad for
171
			// performance also on other DBMSs. For the proper operation of this function,
172
			// it doesn't matter if we fetch data for some extra albums.
173
			$albumIds = ($allAlbums || \count($albums) >= self::MAX_SQL_ALBUM_ARGS)
174
					? null : Util::extractIds($albums);
175
176
			$artists = $this->mapper->getPerformingArtistsByAlbumId($albumIds, $userId);
177
			$years = $this->mapper->getYearsByAlbumId($albumIds, $userId);
178
			$diskCounts = $this->mapper->getDiscCountByAlbumId($albumIds, $userId);
179
			$genres = $this->mapper->getGenresByAlbumId($albumIds, $userId);
180
181
			foreach ($albums as &$album) {
182
				$albumId = $album->getId();
183
				$album->setArtistIds($artists[$albumId] ?? []);
184
				$album->setNumberOfDisks($diskCounts[$albumId] ?? 1);
185
				$album->setGenres($genres[$albumId] ?? null);
186
				$album->setYears($years[$albumId] ?? null);
187
			}
188
		}
189
		return $albums;
190
	}
191
192
	/**
193
	 * Returns the count of albums where the given Artist is featured in
194
	 * @param integer $artistId
195
	 * @return integer
196
	 */
197
	public function countByArtist(int $artistId) : int {
198
		return $this->mapper->countByArtist($artistId);
199
	}
200
201
	/**
202
	 * Returns the count of albums where the given artist is the album artist
203
	 * @param integer $artistId
204
	 * @return integer
205
	 */
206
	public function countByAlbumArtist(int $artistId) : int {
207
		return $this->mapper->countByAlbumArtist($artistId);
208
	}
209
210
	public function findAlbumOwner(int $albumId) : string {
211
		$entities = $this->findById([$albumId]);
212
		if (\count($entities) != 1) {
213
			throw new BusinessLayerException(
214
					'Expected to find one album but got ' . \count($entities));
215
		} else {
216
			return $entities[0]->getUserId();
217
		}
218
	}
219
220
	/**
221
	 * Adds an album if it does not exist already or updates an existing album
222
	 * @param string|null $name the name of the album
223
	 * @param integer $albumArtistId
224
	 * @param string $userId
225
	 * @return Album The added/updated album
226
	 */
227
	public function addOrUpdateAlbum(?string $name, int $albumArtistId, string $userId) : Album {
228
		$album = new Album();
229
		$album->setName(Util::truncate($name, 256)); // some DB setups can't truncate automatically to column max size
230
		$album->setUserId($userId);
231
		$album->setAlbumArtistId($albumArtistId);
232
233
		// Generate hash from the set of fields forming the album identity to prevent duplicates.
234
		// The uniqueness of album name is evaluated in case-insensitive manner.
235
		$lowerName = \mb_strtolower($album->getName() ?? '');
236
		$hash = \hash('md5', "$lowerName|$albumArtistId");
237
		$album->setHash($hash);
238
239
		return $this->mapper->updateOrInsert($album);
240
	}
241
242
	/**
243
	 * Check if given file is used as cover for the given album
244
	 * @param int $albumId
245
	 * @param int[] $fileIds
246
	 * @return boolean
247
	 */
248
	public function albumCoverIsOneOfFiles(int $albumId, array $fileIds) : bool {
249
		$albums = $this->findById([$albumId]);
250
		return (\count($albums) && \in_array($albums[0]->getCoverFileId(), $fileIds));
251
	}
252
253
	/**
254
	 * updates the cover for albums in the specified folder without cover
255
	 * @param integer $coverFileId the file id of the cover image
256
	 * @param integer $folderId the file id of the folder where the albums are looked from
257
	 * @return boolean True if one or more albums were influenced
258
	 */
259
	public function updateFolderCover(int $coverFileId, int $folderId) : bool {
260
		return $this->mapper->updateFolderCover($coverFileId, $folderId);
261
	}
262
263
	/**
264
	 * set cover file for a specified album
265
	 * @param int|null $coverFileId the file id of the cover image
266
	 * @param int $albumId the id of the album to be modified
267
	 */
268
	public function setCover(?int $coverFileId, int $albumId) {
269
		$this->mapper->setCover($coverFileId, $albumId);
270
	}
271
272
	/**
273
	 * removes the cover art from albums, replacement covers will be searched in a background task
274
	 * @param integer[] $coverFileIds the file IDs of the cover images
275
	 * @param string[]|null $userIds the users whose music library is targeted; all users are targeted if omitted
276
	 * @return Album[] albums which got modified, empty array if none
277
	 */
278
	public function removeCovers(array $coverFileIds, array $userIds=null) : array {
279
		return $this->mapper->removeCovers($coverFileIds, $userIds);
280
	}
281
282
	/**
283
	 * try to find cover arts for albums without covers
284
	 * @param string|null $userId target user; omit to target all users
285
	 * @return string[] users whose collections got modified
286
	 */
287
	public function findCovers(string $userId = null) : array {
288
		$affectedUsers = [];
289
		$albums = $this->mapper->getAlbumsWithoutCover($userId);
290
		foreach ($albums as $album) {
291
			if ($this->mapper->findAlbumCover($album['albumId'], $album['parentFolderId'])) {
292
				$affectedUsers[$album['userId']] = 1;
293
			}
294
		}
295
		return \array_keys($affectedUsers);
296
	}
297
298
	/**
299
	 * Given an array of track IDs, find corresponding uniqu album IDs, including only
300
	 * those album which have a cover art set.
301
	 * @param int[] $trackIds
302
	 * @return Album[] *Partial* albums, without any injected extra fields
303
	 */
304
	public function findAlbumsWithCoversForTracks(array $trackIds, string $userId, int $limit) : array {
305
		if (\count($trackIds) === 0) {
306
			return [];
307
		} else {
308
			$result = [];
309
			$idChunks = \array_chunk($trackIds, self::MAX_SQL_ALBUM_ARGS);
310
			foreach ($idChunks as $idChunk) {
311
				$resultChunk = $this->mapper->findAlbumsWithCoversForTracks($idChunk, $userId, $limit);
312
				$result = \array_merge($result, $resultChunk);
313
				$limit -= \count($resultChunk);
314
				if ($limit <= 0) {
315
					break;
316
				}
317
			}
318
			return $result;
319
		}
320
	}
321
322
	/**
323
	 * Given an array of Track objects, inject the corresponding Album object to each of them
324
	 * @param Track[] $tracks (in|out)
325
	 */
326
	public function injectAlbumsToTracks(array &$tracks, string $userId) {
327
		$albumIds = [];
328
329
		// get unique album IDs
330
		foreach ($tracks as $track) {
331
			$albumIds[$track->getAlbumId()] = 1;
332
		}
333
		$albumIds = \array_keys($albumIds);
334
335
		// get the corresponding entities from the business layer
336
		if (\count($albumIds) < $this->count($userId)) {
337
			$albums = $this->findById($albumIds, $userId);
338
		} else {
339
			$albums = $this->findAll($userId);
340
		}
341
342
		// create hash tables "id => entity" for the albums for fast access
343
		$albumMap = Util::createIdLookupTable($albums);
344
345
		// finally, set the references on the tracks
346
		foreach ($tracks as &$track) {
347
			$track->setAlbum($albumMap[$track->getAlbumId()] ?? new Album());
348
		}
349
	}
350
}
351