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