AlbumBusinessLayer::findAll()   B
last analyzed

Complexity

Conditions 7
Paths 12

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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

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